From 6a5b42794f68d4baae16ab1baf78a73b4046b39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 29 Sep 2025 14:43:30 +0200 Subject: [PATCH 1/6] package with pyproject.toml + bump python --- MANIFEST.in | 1 - Makefile | 25 ++++++++++---- bors.toml | 4 --- flake.nix | 10 ++---- pyproject.toml | 53 +++++++++++++++++++++++++++- setup.cfg | 16 --------- setup.py | 94 -------------------------------------------------- tox.ini | 2 +- 8 files changed, 75 insertions(+), 130 deletions(-) delete mode 100644 bors.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in index 6b05c80..2589c98 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -exclude setup.cfg include *.txt include *.rst recursive-include doc *.rst diff --git a/Makefile b/Makefile index d5d3b00..6570e03 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYTHON ?= python3.11 +PYTHON ?= python3.13 REMOTE = git@github.com:Mic92/python-mpd2 VERSION = $(shell $(PYTHON) -c "import mpd; print('.'.join(map(str,mpd.VERSION)))") @@ -7,12 +7,25 @@ test: release: test test "$(shell git symbolic-ref --short HEAD)" = "master" || (echo "not on master branch"; exit 1) git pull --rebase origin master - $(PYTHON) setup.py sdist bdist_wheel - $(PYTHON) -m twine check dist/python-mpd2-$(VERSION).tar.gz dist/python_mpd2-$(VERSION)-py2.py3-none-any.whl + $(PYTHON) -m build + $(PYTHON) -m twine check dist/python-mpd2-$(VERSION).tar.gz dist/python_mpd2-$(VERSION)-py3-none-any.whl git tag "v$(VERSION)" git push --tags git@github.com:Mic92/python-mpd2 "v$(VERSION)" - $(PYTHON) -m twine upload --repository python-mpd2 dist/python-mpd2-$(VERSION).tar.gz dist/python_mpd2-$(VERSION)-py2.py3-none-any.whl + $(PYTHON) -m twine upload --repository python-mpd2 dist/python-mpd2-$(VERSION).tar.gz dist/python_mpd2-$(VERSION)-py3-none-any.whl clean: - $(PYTHON) setup.py clean + rm -rf dist build *.egg-info -.PHONY: test release clean +bump-version: + @if [ -z "$(NEW_VERSION)" ]; then \ + echo "Usage: make bump-version NEW_VERSION=x.y.z"; \ + exit 1; \ + fi + @echo "Bumping version to $(NEW_VERSION)..." + @# Convert x.y.z to (x, y, z) for mpd/base.py + @VERSION_TUPLE=$$(echo "$(NEW_VERSION)" | sed 's/\./,\ /g'); \ + sed -i.bak "s/^VERSION = (.*/VERSION = ($$VERSION_TUPLE)/" mpd/base.py && rm mpd/base.py.bak + @# Update version in pyproject.toml + @sed -i.bak 's/^version = .*/version = "$(NEW_VERSION)"/' pyproject.toml && rm pyproject.toml.bak + @echo "Version bumped to $(NEW_VERSION)" + +.PHONY: test release clean bump-version diff --git a/bors.toml b/bors.toml deleted file mode 100644 index 501cb5f..0000000 --- a/bors.toml +++ /dev/null @@ -1,4 +0,0 @@ -cut_body_after = "" # don't include text from the PR body in the merge commit message -status = [ - "tests" -] diff --git a/flake.nix b/flake.nix index e847c4c..ba5b501 100644 --- a/flake.nix +++ b/flake.nix @@ -14,19 +14,15 @@ devShells.default = pkgs.mkShell { packages = with pkgs; [ bashInteractive - python39 python310 python311 - (python312.withPackages(ps: [ps.setuptools ps.tox ps.wheel])) - python313 + python312 + (python313.withPackages(ps: [ps.setuptools ps.tox ps.wheel ps.build])) + python314 pypy3 twine mypy ]; - shellHook = '' - # breaks python36/python37 - unset _PYTHON_SYSCONFIGDATA_NAME - ''; }; }; }); diff --git a/pyproject.toml b/pyproject.toml index 37bdf25..c474146 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,56 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-mpd2" +version = "3.1.1" +description = "A Python MPD client library" +readme = "README.rst" +authors = [ + {name = "Joerg Thalheim", email = "joerg@thalheim.io"}, +] +maintainers = [ + {name = "Joerg Thalheim", email = "joerg@thalheim.io"}, +] +license = {text = "GNU Lesser General Public License v3 (LGPLv3)"} +keywords = ["mpd"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.10" +dependencies = [] + +[project.optional-dependencies] +twisted = ["Twisted"] +test = [ + "tox", + "Twisted", +] + +[project.urls] +Homepage = "https://github.com/Mic92/python-mpd2" +Repository = "https://github.com/Mic92/python-mpd2" +Issues = "https://github.com/Mic92/python-mpd2/issues" + +[tool.setuptools] +packages = ["mpd"] +zip-safe = true + + +[tool.setuptools.package-data] +"*" = ["py.typed"] + + [tool.mypy] -python_version = "3.11" +python_version = "3.10" pretty = true warn_redundant_casts = true disallow_untyped_calls = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 17ba927..0000000 --- a/setup.cfg +++ /dev/null @@ -1,16 +0,0 @@ -[sdist] -formats = gztar - -[bdist_wheel] -universal=1 - -[build_sphinx] -source-dir = doc/ -build-dir = doc/_build -all_files = 1 - -[upload_sphinx] -upload-dir = doc/_build/html - -[easy_install] - diff --git a/setup.py b/setup.py deleted file mode 100644 index f529765..0000000 --- a/setup.py +++ /dev/null @@ -1,94 +0,0 @@ -#! /usr/bin/env python - -from setuptools import find_packages -from setuptools import setup -from setuptools.command.test import test as TestCommand -import mpd -import os -import sys - - -if sys.version_info[0] == 2: - from io import open - - -VERSION = ".".join(map(str, mpd.VERSION)) - -CLASSIFIERS = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Topic :: Software Development :: Libraries :: Python Modules", -] - -LICENSE = """\ -Copyright (C) 2008-2010 J. Alexander Treuman -Copyright (C) 2012-2017 Joerg Thalheim -Copyright (C) 2016 Robert Niederreiter - -python-mpd2 is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -python-mpd2 is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License -along with python-mpd2. If not, see .\ -""" - - -class Tox(TestCommand): - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import tox - errno = tox.cmdline(self.test_args) - sys.exit(errno) - - -def read(fname): - with open(os.path.join(os.path.dirname(__file__), fname), - encoding="utf8") as fd: - return fd.read() - - -setup( - name="python-mpd2", - version=VERSION, - python_requires='>=3.6', - description="A Python MPD client library", - long_description=read('README.rst'), - long_description_content_type='text/x-rst', - classifiers=CLASSIFIERS, - author="Joerg Thalheim", - author_email="joerg@thalheim.io", - license="GNU Lesser General Public License v3 (LGPLv3)", - url="https://github.com/Mic92/python-mpd2", - packages=find_packages(), - zip_safe=True, - keywords=["mpd"], - test_suite="mpd.tests", - tests_require=[ - 'tox', - 'Twisted' - ], - cmdclass={ - 'test': Tox - }, - extras_require={ - 'twisted': ['Twisted'] - } -) - -# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: diff --git a/tox.ini b/tox.ini index 4846d11..5c52f52 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39,py310,py311,py312,py313,pypy3 +envlist = py310,py311,py312,py313,py314,pypy3 [testenv] deps = coverage From 117694881bfc7d367bb0c28155491c763df42220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 29 Sep 2025 14:49:24 +0200 Subject: [PATCH 2/6] reformat with ruff --- doc/conf.py | 140 +++++++------ doc/generate_command_reference.py | 29 +-- examples/asyncio_example.py | 7 +- examples/coverart.py | 16 +- examples/errorhandling.py | 26 +-- examples/locking.py | 4 +- examples/logger.py | 1 + examples/multitags.py | 16 +- examples/randomqueue.py | 8 +- examples/stats.py | 28 ++- examples/stickers.py | 39 ++-- examples/twisted_example.py | 20 +- mpd/asyncio.py | 35 +++- mpd/base.py | 29 ++- mpd/tests.py | 332 +++++++++++++++++------------- mpd/twisted.py | 16 +- pyproject.toml | 4 + 17 files changed, 431 insertions(+), 319 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 925688a..3a32954 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -22,27 +22,27 @@ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.viewcode'] +extensions = ["sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'python-mpd2' -copyright = u'2013, Jörg Thalheim' +project = "python-mpd2" +copyright = "2013, Jörg Thalheim" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -55,161 +55,164 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-mpd2doc' +htmlhelp_basename = "python-mpd2doc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-mpd2.tex', u'python-mpd2 Documentation', - u'Jörg Thalheim', 'manual'), + ( + "index", + "python-mpd2.tex", + "python-mpd2 Documentation", + "Jörg Thalheim", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -217,12 +220,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'python-mpd2', u'python-mpd2 Documentation', - [u'Jörg Thalheim'], 1) + ("index", "python-mpd2", "python-mpd2 Documentation", ["Jörg Thalheim"], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -231,19 +233,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-mpd2', u'python-mpd2 Documentation', - u'Jörg Thalheim', 'python-mpd2', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "python-mpd2", + "python-mpd2 Documentation", + "Jörg Thalheim", + "python-mpd2", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/doc/generate_command_reference.py b/doc/generate_command_reference.py index 1e9034f..1cdcc8d 100644 --- a/doc/generate_command_reference.py +++ b/doc/generate_command_reference.py @@ -5,6 +5,7 @@ import os.path from textwrap import TextWrapper import urllib.request + try: from lxml import etree except ImportError: @@ -17,15 +18,15 @@ def get_text(elements, itemize=False): paragraphs = [] - highlight_elements = ['varname', 'parameter'] + highlight_elements = ["varname", "parameter"] strip_elements = [ - 'returnvalue', - 'command', - 'link', - 'footnote', - 'simpara', - 'footnoteref', - 'function' + "returnvalue", + "command", + "link", + "footnote", + "simpara", + "footnoteref", + "function", ] + highlight_elements for element in elements: # put "Since MPD version..." in parenthese @@ -42,17 +43,18 @@ def get_text(elements, itemize=False): else: initial_indent = " " subsequent_indent = " " - wrapper = TextWrapper(subsequent_indent=subsequent_indent, - initial_indent=initial_indent) + wrapper = TextWrapper( + subsequent_indent=subsequent_indent, initial_indent=initial_indent + ) text = element.text.replace("\n", " ").strip() - text = re.subn(r'\s+', ' ', text)[0] + text = re.subn(r"\s+", " ", text)[0] paragraphs.append(wrapper.fill(text)) return "\n\n".join(paragraphs) def main(url): header_file = os.path.join(SCRIPT_PATH, "commands_header.txt") - with open(header_file, 'r') as f: + with open(header_file, "r") as f: print(f.read()) r = urllib.request.urlopen(url) @@ -97,7 +99,7 @@ def main(url): cmd += "_" + subcommand print(".. function:: MPDClient." + cmd + "(" + args + ")") description = get_text(entry.xpath("listitem/para")) - description = re.sub(r':$', r'::', description, flags=re.MULTILINE) + description = re.sub(r":$", r"::", description, flags=re.MULTILINE) print("\n") print(description) @@ -110,6 +112,7 @@ def main(url): print(get_text(item.xpath("para"), itemize=True)) print("\n") + if __name__ == "__main__": url = "https://raw.githubusercontent.com/MusicPlayerDaemon/MPD/master/doc/protocol.xml" if len(sys.argv) > 1: diff --git a/examples/asyncio_example.py b/examples/asyncio_example.py index 87c9fb7..2573a25 100644 --- a/examples/asyncio_example.py +++ b/examples/asyncio_example.py @@ -2,6 +2,7 @@ from mpd.asyncio import MPDClient + async def main(): print("Create MPD client") client = MPDClient() @@ -10,7 +11,7 @@ async def main(): client.disconnect() try: - await client.connect('localhost', 6600) + await client.connect("localhost", 6600) except Exception as e: print("Connection failed:", e) return @@ -28,6 +29,7 @@ async def main(): print(list(await client.commands())) import time + start = time.time() for x in await client.listall(): print("sync:", x) @@ -59,5 +61,6 @@ async def main(): print("Enough changes, quitting") break -if __name__ == '__main__': + +if __name__ == "__main__": asyncio.run(main()) diff --git a/examples/coverart.py b/examples/coverart.py index 33bded0..f6cc72e 100644 --- a/examples/coverart.py +++ b/examples/coverart.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # IMPORTS -from mpd import (MPDClient, CommandError, FailureResponseCode) +from mpd import MPDClient, CommandError, FailureResponseCode from socket import error as SocketError from sys import exit from PIL import Image @@ -10,10 +10,10 @@ ## SETTINGS ## -HOST = 'localhost' -PORT = '6600' +HOST = "localhost" +PORT = "6600" PASSWORD = False -SONG = '' +SONG = "" ### client = MPDClient() @@ -43,15 +43,15 @@ except CommandError: exit(1) -if 'binary' not in cover_art: +if "binary" not in cover_art: # The song exists but has no embedded cover art print("No embedded art found!") exit(1) -if 'type' in cover_art: - print("Cover art of type " + cover_art['type']) +if "type" in cover_art: + print("Cover art of type " + cover_art["type"]) -with Image.open(BytesIO(cover_art['binary'])) as img: +with Image.open(BytesIO(cover_art["binary"])) as img: img.show() client.disconnect() diff --git a/examples/errorhandling.py b/examples/errorhandling.py index cc6c84a..a8c2add 100644 --- a/examples/errorhandling.py +++ b/examples/errorhandling.py @@ -1,9 +1,9 @@ #! /usr/bin/env python # -#Introduction +# Introduction # -#A python program that continuously polls for song info. Demonstrates how and where to handle errors -#Details +# A python program that continuously polls for song info. Demonstrates how and where to handle errors +# Details # from mpd import MPDClient, MPDError, CommandError @@ -27,16 +27,14 @@ def connect(self): # Catch socket errors except IOError as err: errno, strerror = err - raise PollerError("Could not connect to '%s': %s" % - (self._host, strerror)) + raise PollerError("Could not connect to '%s': %s" % (self._host, strerror)) # Catch all other possible errors # ConnectionError and ProtocolError are always fatal. Others may not # be, but we don't know how to handle them here, so treat them as if # they are instead of ignoring them. except MPDError as e: - raise PollerError("Could not connect to '%s': %s" % - (self._host, e)) + raise PollerError("Could not connect to '%s': %s" % (self._host, e)) if self._password: try: @@ -46,15 +44,17 @@ def connect(self): except CommandError as e: # On CommandErrors we have access to the parsed error response # split into errno, offset, command and msg. - raise PollerError("Could not connect to '%s': " - "password commmand failed: [%d] %s" % - (self._host, e.errno, e.msg)) + raise PollerError( + "Could not connect to '%s': " + "password commmand failed: [%d] %s" % (self._host, e.errno, e.msg) + ) # Catch all other possible errors except (MPDError, IOError) as e: - raise PollerError("Could not connect to '%s': " - "error with password command: %s" % - (self._host, e)) + raise PollerError( + "Could not connect to '%s': " + "error with password command: %s" % (self._host, e) + ) def disconnect(self): # Try to tell MPD we're closing the connection first diff --git a/examples/locking.py b/examples/locking.py index ad82aae..62a591e 100644 --- a/examples/locking.py +++ b/examples/locking.py @@ -2,6 +2,7 @@ from random import choice from mpd import MPDClient + class LockableMPDClient(MPDClient): def __init__(self): super(LockableMPDClient, self).__init__() @@ -24,12 +25,13 @@ def __exit__(self, type, value, traceback): client.connect("localhost", 6600) # now whenever you need thread-safe access # use the 'with' statement like this: -with client: # acquire lock +with client: # acquire lock status = client.status() # if you leave the block, the lock is released # it is recommend to leave it soon, # otherwise your other threads will blocked. + # Let's test if it works .... def fetch_playlist(): for i in range(10): diff --git a/examples/logger.py b/examples/logger.py index ee48fe8..9bb560f 100644 --- a/examples/logger.py +++ b/examples/logger.py @@ -1,4 +1,5 @@ import logging, mpd + logging.basicConfig(level=logging.DEBUG) client = mpd.MPDClient() client.connect("localhost", 6600) diff --git a/examples/multitags.py b/examples/multitags.py index c9bb600..76fa2ef 100644 --- a/examples/multitags.py +++ b/examples/multitags.py @@ -1,14 +1,12 @@ -#Multi tag files +# Multi tag files # -#Some tag formats (such as ID3v2 and VorbisComment) support defining the same tag multiple times, mostly for when a song has multiple artists. MPD supports this, and sends each occurrence of a tag to the client. +# Some tag formats (such as ID3v2 and VorbisComment) support defining the same tag multiple times, mostly for when a song has multiple artists. MPD supports this, and sends each occurrence of a tag to the client. # -#When python-mpd encounters the same tag more than once on the same song, it uses a list instead of a string. -#Function to get a string only song object. +# When python-mpd encounters the same tag more than once on the same song, it uses a list instead of a string. +# Function to get a string only song object. def collapse_tags(song): - for tag, value in song.iteritems(): - if isinstance(value, list): - song[tag] = ", ".join(set(value)) - - + for tag, value in song.iteritems(): + if isinstance(value, list): + song[tag] = ", ".join(set(value)) diff --git a/examples/randomqueue.py b/examples/randomqueue.py index 0671d18..eae4214 100644 --- a/examples/randomqueue.py +++ b/examples/randomqueue.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # IMPORTS -from mpd import (MPDClient, CommandError) +from mpd import MPDClient, CommandError from random import choice from socket import error as SocketError from sys import exit @@ -10,8 +10,8 @@ ## SETTINGS ## -HOST = 'localhost' -PORT = '6600' +HOST = "localhost" +PORT = "6600" PASSWORD = False ### @@ -29,7 +29,7 @@ except CommandError: exit(1) -client.add(choice(client.list('file'))) +client.add(choice(client.list("file"))) client.disconnect() # VIM MODLINE diff --git a/examples/stats.py b/examples/stats.py index 77f7c38..820b55b 100644 --- a/examples/stats.py +++ b/examples/stats.py @@ -5,15 +5,16 @@ import sys import pprint -from mpd import (MPDClient, CommandError) +from mpd import MPDClient, CommandError from socket import error as SocketError -HOST = 'localhost' -PORT = '6600' +HOST = "localhost" +PORT = "6600" PASSWORD = False ## -CON_ID = {'host':HOST, 'port':PORT} -## +CON_ID = {"host": HOST, "port": PORT} +## + ## Some functions def mpdConnect(client, con_id): @@ -26,6 +27,7 @@ def mpdConnect(client, con_id): return False return True + def mpdAuth(client, secret): """ Authenticate @@ -35,23 +37,26 @@ def mpdAuth(client, secret): except CommandError: return False return True + + ## + def main(): ## MPD object instance client = MPDClient() if mpdConnect(client, CON_ID): - print('Got connected!') + print("Got connected!") else: - print('fail to connect MPD server.') + print("fail to connect MPD server.") sys.exit(1) # Auth if password is set non False if PASSWORD: if mpdAuth(client, PASSWORD): - print('Pass auth!') + print("Pass auth!") else: - print('Error trying to pass auth.') + print("Error trying to pass auth.") client.disconnect() sys.exit(2) @@ -59,15 +64,16 @@ def main(): pp = pprint.PrettyPrinter(indent=4) ## Print out MPD stats & disconnect - print('\nCurrent MPD state:') + print("\nCurrent MPD state:") pp.pprint(client.status()) - print('\nMusic Library stats:') + print("\nMusic Library stats:") pp.pprint(client.stats()) client.disconnect() sys.exit(0) + # Script starts here if __name__ == "__main__": main() diff --git a/examples/stickers.py b/examples/stickers.py index 693c605..94b3ca4 100644 --- a/examples/stickers.py +++ b/examples/stickers.py @@ -1,27 +1,27 @@ -#Descriptio, file=sys.stderrn +# Descriptio, file=sys.stderrn # -#Using this client, one can manipulate and query stickers. The script is essentially a raw interface to the MPD protocol's sticker command, and is used in exactly the same way. -#Examples +# Using this client, one can manipulate and query stickers. The script is essentially a raw interface to the MPD protocol's sticker command, and is used in exactly the same way. +# Examples ## set sticker "foo" to "bar" on "dir/song.mp3" -#sticker.py set dir/song.mp3 foo bar +# sticker.py set dir/song.mp3 foo bar # ## get sticker "foo" on "dir/song.mp3" -#sticker.py get dir/song.mp3 foo +# sticker.py get dir/song.mp3 foo # ## list all stickers on "dir/song.mp3" -#sticker.py list dir/song.mp3 +# sticker.py list dir/song.mp3 # ## find all files with sticker "foo" in "dir" -#sticker.py find dir foo +# sticker.py find dir foo # ## find all files with sticker "foo" -#sticker.py find / foo +# sticker.py find / foo # ## delete sticker "foo" from "dir/song.mp3" -#sticker.py delete dir/song.mp3 foo +# sticker.py delete dir/song.mp3 foo # -#sticker.py +# sticker.py #! /usr/bin/env python @@ -65,9 +65,11 @@ def main(action, uri, name, value): if __name__ == "__main__": - parser = OptionParser(usage="%prog action args", version="0.1", - description="Manipulate and query " - "MPD song stickers.") + parser = OptionParser( + usage="%prog action args", + version="0.1", + description="Manipulate and query MPD song stickers.", + ) options, args = parser.parse_args() if len(args) < 1: @@ -98,11 +100,14 @@ def main(action, uri, name, value): try: main(action, uri, name, value) except SocketError as e: - print("%s: error with connection to MPD: %s" % \ - (parser.get_prog_name(), e[1]), file=stderr) + print( + "%s: error with connection to MPD: %s" % (parser.get_prog_name(), e[1]), + file=stderr, + ) except MPDError as e: - print("%s: error executing action: %s" % \ - (parser.get_prog_name(), e), file=stderr) + print( + "%s: error executing action: %s" % (parser.get_prog_name(), e), file=stderr + ) # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: diff --git a/examples/twisted_example.py b/examples/twisted_example.py index ef4a55e..3e18d98 100644 --- a/examples/twisted_example.py +++ b/examples/twisted_example.py @@ -12,43 +12,41 @@ def __init__(self, protocol): def __call__(self, result): # idle result callback - print('Subsystems: {}'.format(list(result))) + print("Subsystems: {}".format(list(result))) def status_success(result): # status query success - print('Status success: {}'.format(result)) + print("Status success: {}".format(result)) def status_error(result): # status query failure - print('Status error: {}'.format(result)) + print("Status error: {}".format(result)) # query player status - self.protocol.status()\ - .addCallback(status_success)\ - .addErrback(status_error) + self.protocol.status().addCallback(status_success).addErrback(status_error) class MPDClientFactory(protocol.ClientFactory): protocol = MPDProtocol def buildProtocol(self, addr): - print('Create MPD protocol') + print("Create MPD protocol") protocol = self.protocol() protocol.factory = self protocol.idle_result = MPDApp(protocol) return protocol def clientConnectionFailed(self, connector, reason): - print('Connection failed - goodbye!: {}'.format(reason)) + print("Connection failed - goodbye!: {}".format(reason)) reactor.stop() def clientConnectionLost(self, connector, reason): - print('Connection lost - goodbye!: {}'.format(reason)) + print("Connection lost - goodbye!: {}".format(reason)) if reactor.running: reactor.stop() -if __name__ == '__main__': +if __name__ == "__main__": factory = MPDClientFactory() - reactor.connectTCP('localhost', 6600, factory) + reactor.connectTCP("localhost", 6600, factory) reactor.run() diff --git a/mpd/asyncio.py b/mpd/asyncio.py index 8b05c61..d6cc8bc 100644 --- a/mpd/asyncio.py +++ b/mpd/asyncio.py @@ -21,11 +21,28 @@ import asyncio import warnings from functools import partial -from typing import (Any, AsyncIterator, Callable, Iterable, List, - Optional, Set, Tuple, Union, Dict, cast) - -from mpd.base import (ERROR_PREFIX, SUCCESS, CommandError, CommandListError, - ConnectionError, CallableWithCommands) +from typing import ( + Any, + AsyncIterator, + Callable, + Iterable, + List, + Optional, + Set, + Tuple, + Union, + Dict, + cast, +) + +from mpd.base import ( + ERROR_PREFIX, + SUCCESS, + CommandError, + CommandListError, + ConnectionError, + CallableWithCommands, +) from mpd.base import MPDClient as SyncMPDClient from mpd.base import MPDClientBase, ProtocolError, mpd_command_provider @@ -121,7 +138,9 @@ class CommandResultIterable(BaseCommandResult): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.__spooled_lines: asyncio.Queue[Union[str, None, Exception]] = asyncio.Queue() + self.__spooled_lines: asyncio.Queue[Union[str, None, Exception]] = ( + asyncio.Queue() + ) def _feed_line(self, line: Union[str, None]) -> None: self.__spooled_lines.put_nowait(line) @@ -459,13 +478,13 @@ async def _read_line(self) -> Optional[str]: return None return line - async def _parse_objects_direct( # type: ignore + async def _parse_objects_direct( # type: ignore self, lines: "asyncio.Queue[str]", delimiters: List[str] = [], lookup_delimiter: bool = False, ) -> AsyncIterator[Dict[str, str]]: - obj : Dict[str, Any] = {} + obj: Dict[str, Any] = {} while True: line = await lines.get() if isinstance(line, BaseException): diff --git a/mpd/base.py b/mpd/base.py index bd4f250..275fa9b 100644 --- a/mpd/base.py +++ b/mpd/base.py @@ -23,8 +23,19 @@ import sys import warnings from enum import Enum -from typing import (IO, Any, Callable, Dict, Iterator, List, Optional, Tuple, Iterable, - Type, Union) +from typing import ( + IO, + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Tuple, + Iterable, + Type, + Union, +) VERSION = (3, 1, 1) HELLO_PREFIX = "OK MPD " @@ -123,8 +134,7 @@ def __init__(self, *commands: str, **kwargs: bool) -> None: self.is_binary = kwargs.pop("is_binary", False) if kwargs: raise AttributeError( - "mpd_commands() got unexpected keyword" - " arguments %s" % ",".join(kwargs) + "mpd_commands() got unexpected keyword arguments %s" % ",".join(kwargs) ) def __call__(self, ob: Any) -> CallableWithCommands: @@ -155,8 +165,7 @@ def __init__(self, use_unicode: Optional[bool] = None) -> None: self.iterate = False if use_unicode is not None: warnings.warn( - "use_unicode parameter to ``MPDClientBase`` constructor is " - "deprecated", + "use_unicode parameter to ``MPDClientBase`` constructor is deprecated", DeprecationWarning, stacklevel=2, ) @@ -165,7 +174,7 @@ def __init__(self, use_unicode: Optional[bool] = None) -> None: @property def use_unicode(self) -> bool: warnings.warn( - "``use_unicode`` is deprecated: python-mpd 2.x always uses " "Unicode", + "``use_unicode`` is deprecated: python-mpd 2.x always uses Unicode", DeprecationWarning, stacklevel=2, ) @@ -184,12 +193,12 @@ def noidle(self) -> None: def command_list_ok_begin(self) -> None: raise NotImplementedError( - "Abstract ``MPDClientBase`` does not implement " "``command_list_ok_begin``" + "Abstract ``MPDClientBase`` does not implement ``command_list_ok_begin``" ) def command_list_end(self) -> None: raise NotImplementedError( - "Abstract ``MPDClientBase`` does not implement " "``command_list_end``" + "Abstract ``MPDClientBase`` does not implement ``command_list_end``" ) def _reset(self) -> None: @@ -523,7 +532,7 @@ class MPDClient(MPDClientBase): def __init__(self, use_unicode: Optional[bool] = None) -> None: if use_unicode is not None: warnings.warn( - "use_unicode parameter to ``MPDClient`` constructor is " "deprecated", + "use_unicode parameter to ``MPDClient`` constructor is deprecated", DeprecationWarning, stacklevel=2, ) diff --git a/mpd/tests.py b/mpd/tests.py index 9789c86..6cbc73a 100755 --- a/mpd/tests.py +++ b/mpd/tests.py @@ -39,7 +39,6 @@ class TestMPDClient(unittest.TestCase): - longMessage = True def setUp(self) -> None: @@ -106,10 +105,10 @@ def test_duplicate_tags(self) -> None: def test_parse_nothing(self) -> None: self.MPDWillReturn("OK\n", "OK\n") - self.assertIsNone(self.client.ping()) + self.assertIsNone(self.client.ping()) self.assertMPDReceived("ping\n") - self.assertIsNone(self.client.clearerror()) + self.assertIsNone(self.client.clearerror()) self.assertMPDReceived("clearerror\n") def test_parse_list(self) -> None: @@ -117,10 +116,17 @@ def test_parse_list(self) -> None: "tagtype: Artist\n", "tagtype: ArtistSort\n", "tagtype: Album\n", "OK\n" ) - result = self.client.tagtypes() + result = self.client.tagtypes() self.assertMPDReceived("tagtypes\n") self.assertIsInstance(result, list) - self.assertEqual(result, ["Artist", "ArtistSort", "Album",]) + self.assertEqual( + result, + [ + "Artist", + "ArtistSort", + "Album", + ], + ) def test_parse_list_groups(self) -> None: self.MPDWillReturn( @@ -130,7 +136,7 @@ def test_parse_list_groups(self) -> None: "OK\n", ) - result = self.client.list("album") + result = self.client.list("album") self.assertMPDReceived('list "album"\n') self.assertIsInstance(result, list) self.assertEqual( @@ -151,7 +157,7 @@ def test_parse_list_groups(self) -> None: "OK\n", ) - result = self.client.list("album", "group", "artist") + result = self.client.list("album", "group", "artist") self.assertMPDReceived('list "album" "group" "artist"\n') self.assertIsInstance(result, list) self.assertEqual( @@ -168,24 +174,24 @@ def test_parse_list_groups(self) -> None: def test_parse_item(self) -> None: self.MPDWillReturn("updating_db: 42\n", "OK\n") - self.assertIsNotNone(self.client.update()) + self.assertIsNotNone(self.client.update()) def test_parse_object(self) -> None: # XXX: _read_objects() doesn't wait for the final OK self.MPDWillReturn("volume: 63\n", "OK\n") - status = self.client.status() + status = self.client.status() self.assertMPDReceived("status\n") self.assertIsInstance(status, dict) # XXX: _read_objects() doesn't wait for the final OK self.MPDWillReturn("OK\n") - stats = self.client.stats() + stats = self.client.stats() self.assertMPDReceived("stats\n") self.assertIsInstance(stats, dict) def test_parse_songs(self) -> None: self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n") - playlist = self.client.playlistinfo() + playlist = self.client.playlistinfo() self.assertMPDReceived("playlistinfo\n") self.assertIsInstance(playlist, list) @@ -200,7 +206,7 @@ def test_readcomments(self) -> None: self.MPDWillReturn( "major_brand: M4V\n", "minor_version: 1\n", "lyrics: Lalala\n", "OK\n" ) - comments = self.client.readcomments() + comments = self.client.readcomments() self.assertMPDReceived("readcomments\n") self.assertEqual(comments["major_brand"], "M4V") self.assertEqual(comments["minor_version"], "1") @@ -209,7 +215,7 @@ def test_readcomments(self) -> None: def test_iterating(self) -> None: self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n") self.client.iterate = True - playlist = self.client.playlistinfo() + playlist = self.client.playlistinfo() self.assertMPDReceived("playlistinfo\n") self.assertIsInstance(playlist, types.GeneratorType) for song in playlist: @@ -224,7 +230,7 @@ def test_add_and_remove_command(self) -> None: self.client.add_command("awesome command", mpd.MPDClient._parse_nothing) self.assertTrue(hasattr(self.client, "awesome_command")) # should be unknown by mpd - self.assertRaises(mpd.CommandError, self.client.awesome_command) + self.assertRaises(mpd.CommandError, self.client.awesome_command) self.client.remove_command("awesome_command") self.assertFalse(hasattr(self.client, "awesome_command")) @@ -234,30 +240,30 @@ def test_add_and_remove_command(self) -> None: def test_partitions(self) -> None: self.MPDWillReturn("partition: default\n", "partition: partition2\n", "OK\n") - partitions = self.client.listpartitions() + partitions = self.client.listpartitions() self.assertMPDReceived("listpartitions\n") self.assertEqual( [ {"partition": "default"}, {"partition": "partition2"}, ], - partitions + partitions, ) self.MPDWillReturn("OK\n") - self.assertIsNone(self.client.newpartition("Another Partition")) + self.assertIsNone(self.client.newpartition("Another Partition")) self.assertMPDReceived('newpartition "Another Partition"\n') self.MPDWillReturn("OK\n") - self.assertIsNone(self.client.partition("Another Partition")) + self.assertIsNone(self.client.partition("Another Partition")) self.assertMPDReceived('partition "Another Partition"\n') self.MPDWillReturn("OK\n") - self.assertIsNone(self.client.delpartition("Another Partition")) + self.assertIsNone(self.client.delpartition("Another Partition")) self.assertMPDReceived('delpartition "Another Partition"\n') self.MPDWillReturn("OK\n") - self.assertIsNone(self.client.moveoutput("My ALSA Device")) + self.assertIsNone(self.client.moveoutput("My ALSA Device")) self.assertMPDReceived('moveoutput "My ALSA Device"\n') def test_list_group(self) -> None: @@ -268,9 +274,9 @@ def test_list_group(self) -> None: "Date: 1974\n", "Album: Autobahn\n", "Album: Autobahn (2009 Der Katalog)\n", - "OK\n" + "OK\n", ) - grouped_list = self.client.list( + grouped_list = self.client.list( "album", "albumartist", "Kraftwerk", "group", "date", "group", "albumartist" ) self.assertMPDReceived( @@ -278,78 +284,86 @@ def test_list_group(self) -> None: ) self.assertEqual( [ - {'albumartist': 'Kraftwerk', 'date': '1970', 'album': 'Tone Float (Unofficial)'}, - {'albumartist': 'Kraftwerk', 'date': '1974', 'album': 'Autobahn'}, - {'albumartist': 'Kraftwerk', 'date': '1974', 'album': 'Autobahn (2009 Der Katalog)'} + { + "albumartist": "Kraftwerk", + "date": "1970", + "album": "Tone Float (Unofficial)", + }, + {"albumartist": "Kraftwerk", "date": "1974", "album": "Autobahn"}, + { + "albumartist": "Kraftwerk", + "date": "1974", + "album": "Autobahn (2009 Der Katalog)", + }, ], - grouped_list + grouped_list, ) def test_client_to_client(self) -> None: # client to client is at this time in beta! self.MPDWillReturn("OK\n") - self.assertIsNone(self.client.subscribe("monty")) + self.assertIsNone(self.client.subscribe("monty")) self.assertMPDReceived('subscribe "monty"\n') self.MPDWillReturn("channel: monty\n", "OK\n") - channels = self.client.channels() + channels = self.client.channels() self.assertMPDReceived("channels\n") self.assertEqual(["monty"], channels) self.MPDWillReturn("OK\n") - self.assertIsNone(self.client.sendmessage("monty", "SPAM")) + self.assertIsNone(self.client.sendmessage("monty", "SPAM")) self.assertMPDReceived('sendmessage "monty" "SPAM"\n') self.MPDWillReturn("channel: monty\n", "message: SPAM\n", "OK\n") - msg = self.client.readmessages() + msg = self.client.readmessages() self.assertMPDReceived("readmessages\n") self.assertEqual(msg, [{"channel": "monty", "message": "SPAM"}]) self.MPDWillReturn("OK\n") - self.assertIsNone(self.client.unsubscribe("monty")) + self.assertIsNone(self.client.unsubscribe("monty")) self.assertMPDReceived('unsubscribe "monty"\n') self.MPDWillReturn("OK\n") - channels = self.client.channels() + channels = self.client.channels() self.assertMPDReceived("channels\n") self.assertEqual([], channels) def test_unicode_as_command_args(self) -> None: self.MPDWillReturn("OK\n") - res = self.client.find("file", "☯☾☝♖✽") + res = self.client.find("file", "☯☾☝♖✽") self.assertIsInstance(res, list) self.assertMPDReceived('find "file" "☯☾☝♖✽"\n') def test_numbers_as_command_args(self) -> None: self.MPDWillReturn("OK\n") - self.client.find("file", 1) + self.client.find("file", 1) self.assertMPDReceived('find "file" "1"\n') def test_commands_without_callbacks(self) -> None: self.MPDWillReturn("\n") - self.client.close() + self.client.close() self.assertMPDReceived("close\n") # XXX: what are we testing here? # looks like reconnection test? self.client._reset() - self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) + self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) def test_set_timeout_on_client(self) -> None: - self.client.timeout = 1 - self.client._sock.settimeout.assert_called_with(1) - self.assertEqual(self.client.timeout, 1) + self.client.timeout = 1 + self.client._sock.settimeout.assert_called_with(1) + self.assertEqual(self.client.timeout, 1) - self.client.timeout = None - self.client._sock.settimeout.assert_called_with(None) - self.assertEqual(self.client.timeout, None) + self.client.timeout = None + self.client._sock.settimeout.assert_called_with(None) + self.assertEqual(self.client.timeout, None) def test_set_timeout_from_connect(self) -> None: - self.client.disconnect() + self.client.disconnect() with warnings.catch_warnings(record=True) as w: - self.client.connect("example.com", 10000, timeout=5) - self.client._sock.settimeout.assert_called_with(5) + self.client.connect("example.com", 10000, timeout=5) + self.client._sock.settimeout.assert_called_with(5) self.assertEqual(len(w), 1) self.assertIn("Use MPDClient.timeout", str(w[0].message)) @@ -358,11 +372,11 @@ def test_set_timeout_from_connect(self) -> None: ) def test_broken_pipe_error(self) -> None: self.MPDWillReturn("volume: 63\n", "OK\n") - self.client._wfile.write.side_effect = BrokenPipeError + self.client._wfile.write.side_effect = BrokenPipeError self.socket_mock.error = Exception with self.assertRaises(mpd.ConnectionError): - self.client.status() + self.client.status() def test_connection_lost(self) -> None: # Simulate a connection lost: the socket returns empty strings @@ -370,24 +384,24 @@ def test_connection_lost(self) -> None: self.socket_mock.error = Exception with self.assertRaises(mpd.ConnectionError): - self.client.status() + self.client.status() self.socket_mock.unpack.assert_called() # consistent behaviour, solves bug #11 (github) with self.assertRaises(mpd.ConnectionError): - self.client.status() + self.client.status() self.socket_mock.unpack.assert_called() self.assertIs(self.client._sock, None) @unittest.skipIf( sys.version_info < (3, 0), - "Automatic decoding/encoding from the socket is only " "available in Python 3", + "Automatic decoding/encoding from the socket is only available in Python 3", ) def test_force_socket_encoding_and_nonbuffering(self) -> None: # Force the reconnection to refill the mock - self.client.disconnect() - self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) + self.client.disconnect() + self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) self.assertEqual( [ mock.call("rb", newline="\n"), @@ -395,31 +409,31 @@ def test_force_socket_encoding_and_nonbuffering(self) -> None: ], # We are only interested into the 2 first entries, # otherwise we get all the readline() & co... - self.client._sock.makefile.call_args_list[0:2], + self.client._sock.makefile.call_args_list[0:2], ) def test_ranges_as_argument(self) -> None: self.MPDWillReturn("OK\n") - self.client.move((1, 2), 2) + self.client.move((1, 2), 2) self.assertMPDReceived('move "1:2" "2"\n') self.MPDWillReturn("OK\n") - self.client.move((1,), 2) + self.client.move((1,), 2) self.assertMPDReceived('move "1:" "2"\n') # old code still works! self.MPDWillReturn("OK\n") - self.client.move("1:2", 2) + self.client.move("1:2", 2) self.assertMPDReceived('move "1:2" "2"\n') # empty ranges self.MPDWillReturn("OK\n") - self.client.rangeid(1, ()) + self.client.rangeid(1, ()) self.assertMPDReceived('rangeid "1" ":"\n') with self.assertRaises(ValueError): self.MPDWillReturn("OK\n") - self.client.move((1, "garbage"), 2) + self.client.move((1, "garbage"), 2) self.assertMPDReceived('move "1:" "2"\n') def test_parse_changes(self) -> None: @@ -436,7 +450,7 @@ def test_parse_changes(self) -> None: "Id: 70\n", "OK\n", ) - res = self.client.plchangesposid(0) + res = self.client.plchangesposid(0) self.assertEqual( [ {"cpos": "0", "id": "66"}, @@ -457,7 +471,7 @@ def test_parse_database(self) -> None: "Last-Modified: 2014-11-02T19:57:00Z\n", "OK\n", ) - self.client.listfiles("/") + self.client.listfiles("/") def test_parse_mounts(self) -> None: self.MPDWillReturn( @@ -467,7 +481,7 @@ def test_parse_mounts(self) -> None: "storage: nfs://192.168.1.4/export/mp3\n", "OK\n", ) - res = self.client.listmounts() + res = self.client.listmounts() self.assertEqual( [ {"mount": "", "storage": "/home/foo/music"}, @@ -480,7 +494,7 @@ def test_parse_neighbors(self) -> None: self.MPDWillReturn( "neighbor: smb://FOO\n", "name: FOO (Samba 4.1.11-Debian)\n", "OK\n" ) - res = self.client.listneighbors() + res = self.client.listneighbors() self.assertEqual( [{"name": "FOO (Samba 4.1.11-Debian)", "neighbor": "smb://FOO"}], res ) @@ -492,7 +506,7 @@ def test_parse_outputs(self) -> None: "outputenabled: 0\n", "OK\n", ) - res = self.client.outputs() + res = self.client.outputs() self.assertEqual( [{"outputenabled": "0", "outputid": "0", "outputname": "My ALSA Device"}], res, @@ -507,7 +521,7 @@ def test_parse_playlist(self) -> None: "4:file: Nirvana - Lithium.mp3\n", "OK\n", ) - res = self.client.playlist() + res = self.client.playlist() self.assertEqual( [ "file: Weezer - Say It Ain't So.mp3", @@ -523,7 +537,7 @@ def test_parse_playlists(self) -> None: self.MPDWillReturn( "playlist: Playlist\n", "Last-Modified: 2016-08-13T10:55:56Z\n", "OK\n" ) - res = self.client.listplaylists() + res = self.client.listplaylists() self.assertEqual( [{"last-modified": "2016-08-13T10:55:56Z", "playlist": "Playlist"}], res ) @@ -543,7 +557,7 @@ def test_parse_plugins(self) -> None: "mime_type: audio/x-vorbis+ogg\n", "OK\n", ) - res = self.client.decoders() + res = self.client.decoders() self.assertEqual( [ { @@ -566,37 +580,37 @@ def test_parse_plugins(self) -> None: def test_parse_raw_stickers(self) -> None: self.MPDWillReturn("sticker: foo=bar\n", "OK\n") - res = self.client._parse_raw_stickers(self.client._read_lines()) + res = self.client._parse_raw_stickers(self.client._read_lines()) self.assertEqual([("foo", "bar")], list(res)) self.MPDWillReturn("sticker: foo=bar\n", "sticker: l=b\n", "OK\n") - res = self.client._parse_raw_stickers(self.client._read_lines()) + res = self.client._parse_raw_stickers(self.client._read_lines()) self.assertEqual([("foo", "bar"), ("l", "b")], list(res)) def test_parse_raw_sticker_with_special_value(self) -> None: self.MPDWillReturn("sticker: foo==uv=vu\n", "OK\n") - res = self.client._parse_raw_stickers(self.client._read_lines()) + res = self.client._parse_raw_stickers(self.client._read_lines()) self.assertEqual([("foo", "=uv=vu")], list(res)) def test_parse_sticket_get_one(self) -> None: self.MPDWillReturn("sticker: foo=bar\n", "OK\n") - res = self.client.sticker_get("song", "baz", "foo") + res = self.client.sticker_get("song", "baz", "foo") self.assertEqual("bar", res) def test_parse_sticket_get_no_sticker(self) -> None: self.MPDWillReturn("ACK [50@0] {sticker} no such sticker\n") self.assertRaises( - mpd.CommandError, self.client.sticker_get, "song", "baz", "foo" + mpd.CommandError, self.client.sticker_get, "song", "baz", "foo" ) def test_parse_sticker_list(self) -> None: self.MPDWillReturn("sticker: foo=bar\n", "sticker: lom=bok\n", "OK\n") - res = self.client.sticker_list("song", "baz") + res = self.client.sticker_list("song", "baz") self.assertEqual({"foo": "bar", "lom": "bok"}, res) # Even with only one sticker, we get a dict self.MPDWillReturn("sticker: foo=bar\n", "OK\n") - res = self.client.sticker_list("song", "baz") + res = self.client.sticker_list("song", "baz") self.assertEqual({"foo": "bar"}, res) def test_command_list(self) -> None: @@ -627,12 +641,12 @@ def test_command_list(self) -> None: "OK\n", ) self.client.command_list_ok_begin() - self.client.clear() - self.client.load("Playlist") - self.client.random(1) - self.client.repeat(1) - self.client.play(0) - self.client.status() + self.client.clear() + self.client.load("Playlist") + self.client.random(1) + self.client.repeat(1) + self.client.play(0) + self.client.status() res = self.client.command_list_end() self.assertEqual(None, res[0]) self.assertEqual(None, res[1]) @@ -669,7 +683,6 @@ def test_command_list(self) -> None: not hasattr(socket, "socketpair"), "Socketpair is not supported on this platform" ) class TestMPDClientSocket(unittest.TestCase): - longMessage = True def setUp(self) -> None: @@ -721,17 +734,17 @@ def test_readbinary_error(self) -> None: self.MPDWillReturnBinary(b"ACK [50@0] {albumart} No file exists\n") self.assertRaises( - mpd.CommandError, lambda: self.client.albumart("a/full/path.mp3") + mpd.CommandError, lambda: self.client.albumart("a/full/path.mp3") ) self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') def test_binary_albumart_disconnect_afterchunk(self) -> None: - self.MPDWillReturnBinary(b"size: 17\nbinary: 3\n" b"\x00\x00\x00\nOK\n") + self.MPDWillReturnBinary(b"size: 17\nbinary: 3\n\x00\x00\x00\nOK\n") # we're expecting a timeout self.assertRaises( - socket.timeout, lambda: self.client.albumart("a/full/path.mp3") + socket.timeout, lambda: self.client.albumart("a/full/path.mp3") ) self.assertMPDReceived( @@ -744,16 +757,16 @@ def test_binary_albumart_disconnect_midchunk(self) -> None: # we're expecting a timeout or error of some form self.assertRaises( - socket.timeout, lambda: self.client.albumart("a/full/path.mp3") + socket.timeout, lambda: self.client.albumart("a/full/path.mp3") ) self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') - self.assertIs(self.client._sock, None) + self.assertIs(self.client._sock, None) def test_binary_albumart_singlechunk_networkmultiwrite(self) -> None: # length 16 expected_binary = ( - b"\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF" + b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf" ) self.MPDWillReturnBinary(b"binary: 16\n") @@ -770,7 +783,7 @@ def test_binary_albumart_singlechunk_networkmultiwrite(self) -> None: def test_binary_albumart_singlechunk_nosize(self) -> None: # length: 16 expected_binary = ( - b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" + b"\x01\x02\x00\x03\x04\x00\xff\x05\x07\x08\x0a\x0f\xf0\xa5\x00\x01" ) self.MPDWillReturnBinary(b"binary: 16\n" + expected_binary + b"\nOK\n") @@ -782,7 +795,7 @@ def test_binary_albumart_singlechunk_nosize(self) -> None: def test_binary_albumart_singlechunk_sizeheader(self) -> None: # length: 16 expected_binary = ( - b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" + b"\x01\x02\x00\x03\x04\x00\xff\x05\x07\x08\x0a\x0f\xf0\xa5\x00\x01" ) self.MPDWillReturnBinary( @@ -796,13 +809,13 @@ def test_binary_albumart_singlechunk_sizeheader(self) -> None: def test_binary_albumart_even_multichunk(self) -> None: # length: 16 each expected_chunk1 = ( - b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" + b"\x01\x02\x00\x03\x04\x00\xff\x05\x07\x08\x0a\x0f\xf0\xa5\x00\x01" ) expected_chunk2 = ( - b"\x0A\x0B\x0C\x0D\x0E\x0F\x10\x1F\x2F\x2D\x33\x0D\x00\x00\x11\x13" + b"\x0a\x0b\x0c\x0d\x0e\x0f\x10\x1f\x2f\x2d\x33\x0d\x00\x00\x11\x13" ) expected_chunk3 = ( - b"\x99\x88\x77\xDD\xD0\xF0\x20\x70\x71\x17\x13\x31\xFF\xFF\xDD\xFF" + b"\x99\x88\x77\xdd\xd0\xf0\x20\x70\x71\x17\x13\x31\xff\xff\xdd\xff" ) expected_binary = expected_chunk1 + expected_chunk2 + expected_chunk3 @@ -828,10 +841,10 @@ def test_binary_albumart_even_multichunk(self) -> None: def test_binary_albumart_odd_multichunk(self) -> None: # lengths: 17, 15, 1 expected_chunk1 = ( - b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01\x13" + b"\x01\x02\x00\x03\x04\x00\xff\x05\x07\x08\x0a\x0f\xf0\xa5\x00\x01\x13" ) expected_chunk2 = ( - b"\x0A\x0B\x0C\x0D\x0E\x0F\x10\x1F\x2F\x2D\x33\x0D\x00\x00\x11" + b"\x0a\x0b\x0c\x0d\x0e\x0f\x10\x1f\x2f\x2d\x33\x0d\x00\x00\x11" ) expected_chunk3 = b"\x99" expected_binary = expected_chunk1 + expected_chunk2 + expected_chunk3 @@ -873,13 +886,13 @@ def test_binary_readpicture_emptyresponse(self) -> None: def test_binary_readpicture_untyped(self) -> None: # length: 16 each expected_chunk1 = ( - b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" + b"\x01\x02\x00\x03\x04\x00\xff\x05\x07\x08\x0a\x0f\xf0\xa5\x00\x01" ) expected_chunk2 = ( - b"\x0A\x0B\x0C\x0D\x0E\x0F\x10\x1F\x2F\x2D\x33\x0D\x00\x00\x11\x13" + b"\x0a\x0b\x0c\x0d\x0e\x0f\x10\x1f\x2f\x2d\x33\x0d\x00\x00\x11\x13" ) expected_chunk3 = ( - b"\x99\x88\x77\xDD\xD0\xF0\x20\x70\x71\x17\x13\x31\xFF\xFF\xDD\xFF" + b"\x99\x88\x77\xdd\xd0\xf0\x20\x70\x71\x17\x13\x31\xff\xff\xdd\xff" ) expected_binary = expected_chunk1 + expected_chunk2 + expected_chunk3 @@ -938,9 +951,7 @@ def test_binary_readpicture_badheaders(self) -> None: + b"\nOK\n" ) - self.assertRaises( - mpd.CommandError, lambda: self.client.readpicture("song.mp3") - ) + self.assertRaises(mpd.CommandError, lambda: self.client.readpicture("song.mp3")) self.assertMPDReceived( b'readpicture "song.mp3" "0"\nreadpicture "song.mp3" "16"\n' @@ -960,8 +971,11 @@ def write(self, data: bytes) -> None: @unittest.skipIf(TWISTED_MISSING, "requires twisted to be installed") class TestMPDProtocol(unittest.TestCase): - - def init_protocol(self, default_idle: bool=True, idle_result: Union[Callable[[List[str]],None],None]=None) -> None: + def init_protocol( + self, + default_idle: bool = True, + idle_result: Union[Callable[[List[str]], None], None] = None, + ) -> None: self.protocol = mpd.MPDProtocol( default_idle=default_idle, idle_result=idle_result ) @@ -971,14 +985,15 @@ def test_create_command(self) -> None: self.init_protocol(default_idle=False) self.assertEqual(self.protocol._create_command("play"), b"play") self.assertEqual( - self.protocol._create_command("rangeid", args=["1", ()]), b'rangeid "1" ":"' # type: ignore + self.protocol._create_command("rangeid", args=["1", ()]), + b'rangeid "1" ":"', # type: ignore ) self.assertEqual( - self.protocol._create_command("rangeid", args=["1", (1,)]), # type: ignore + self.protocol._create_command("rangeid", args=["1", (1,)]), # type: ignore b'rangeid "1" "1:"', ) self.assertEqual( - self.protocol._create_command("rangeid", args=["1", (1, 2)]), # type: ignore + self.protocol._create_command("rangeid", args=["1", (1, 2)]), # type: ignore b'rangeid "1" "1:2"', ) @@ -1072,7 +1087,12 @@ def success(result: Any) -> None: self.protocol.stop() self.protocol.command_list_end().addCallback(success) self.assertEqual( - [b"command_list_ok_begin\n", b"play\n", b"stop\n", b"command_list_end\n",], + [ + b"command_list_ok_begin\n", + b"play\n", + b"stop\n", + b"command_list_end\n", + ], self.protocol.transport.written, ) self.protocol.transport.clear() @@ -1223,10 +1243,12 @@ def success(result: Any) -> None: class AsyncMockServer: def __init__(self) -> None: self._output: asyncio.Queue[bytes] = asyncio.Queue() - self._expectations : List[Tuple[List[bytes], List[bytes]]] = [] + self._expectations: List[Tuple[List[bytes], List[bytes]]] = [] async def get_streams(self) -> Tuple["AsyncMockServer", "AsyncMockServer"]: - result: asyncio.Future[Tuple[AsyncMockServer, AsyncMockServer]] = asyncio.Future() + result: asyncio.Future[Tuple[AsyncMockServer, AsyncMockServer]] = ( + asyncio.Future() + ) result.set_result((self, self)) return await result @@ -1237,7 +1259,9 @@ async def readline(self) -> bytes: async def readexactly(self, length: int) -> bytes: ret = await self._output.get() if len(ret) != length: - self.error("Mock data is not chuncked in the way the client expects to read it") + self.error( + "Mock data is not chuncked in the way the client expects to read it" + ) return ret def write(self, data: bytes) -> None: @@ -1264,13 +1288,15 @@ def _feed(self) -> None: for l in response_lines: self._output.put_nowait(l) - def expect_exchange(self, request_lines: List[bytes], response_lines: List[bytes]) -> None: + def expect_exchange( + self, request_lines: List[bytes], response_lines: List[bytes] + ) -> None: self._expectations.append((request_lines, response_lines)) self._feed() class TestAsyncioMPD(unittest.IsolatedAsyncioTestCase): - async def init_client(self, odd_hello: Optional[List[bytes]]=None) -> None: + async def init_client(self, odd_hello: Optional[List[bytes]] = None) -> None: self.loop = asyncio.get_event_loop() self.mockserver = AsyncMockServer() @@ -1288,9 +1314,7 @@ async def init_client(self, odd_hello: Optional[List[bytes]]=None) -> None: self.client = mpd.asyncio.MPDClient() await self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) - asyncio.open_connection.assert_called_with( - TEST_MPD_HOST, TEST_MPD_PORT - ) + asyncio.open_connection.assert_called_with(TEST_MPD_HOST, TEST_MPD_PORT) def __del__(self) -> None: # Clean up after init_client. (This works for now; if it causes @@ -1337,7 +1361,7 @@ async def test_status(self) -> None: ], ) - status = await self.client.status() + status = await self.client.status() self.assertEqual( status, { @@ -1405,12 +1429,22 @@ async def test_outputs(self) -> None: async def test_list(self) -> None: await self.init_client() self.mockserver.expect_exchange( - [b'list "album"\n'], [b"Album: first\n", b"Album: second\n", b"OK\n",] + [b'list "album"\n'], + [ + b"Album: first\n", + b"Album: second\n", + b"OK\n", + ], ) list_ = self.client.list("album") - expected = iter([{"album": "first"}, {"album": "second"},]) + expected = iter( + [ + {"album": "first"}, + {"album": "second"}, + ] + ) async for o in list_: self.assertEqual(o, next(expected)) @@ -1426,7 +1460,7 @@ async def test_albumart(self) -> None: bytes(range(16)), b"\n", b"OK\n", - ] + ], ) self.mockserver.expect_exchange( [b'albumart "x.mp3" "16"\n'], @@ -1456,7 +1490,7 @@ async def test_readpicture(self) -> None: bytes(range(16)), b"\n", b"OK\n", - ] + ], ) self.mockserver.expect_exchange( [b'readpicture "x.mp3" "16"\n'], @@ -1482,7 +1516,7 @@ async def test_readpicture_empty(self) -> None: [b'readpicture "x.mp3" "0"\n'], [ b"OK\n", - ] + ], ) art = await self.client.readpicture("x.mp3") @@ -1501,32 +1535,41 @@ async def test_mocker(self) -> None: async def test_idle(self) -> None: await self.init_client() self.mockserver.expect_exchange( - [b'idle "database"\n',], - [b"ACK [4@0] don't let you yet but you shouldn't care\n"], - ) + [ + b'idle "database"\n', + ], + [b"ACK [4@0] don't let you yet but you shouldn't care\n"], + ) self.mockserver.expect_exchange( - # code could be smarter and set __in_idle = False and not set - # noidle, but it's not *wrong* either - [b'noidle\n', b'password "1234"\n',], - [b"OK\n"], - ) + # code could be smarter and set __in_idle = False and not set + # noidle, but it's not *wrong* either + [ + b"noidle\n", + b'password "1234"\n', + ], + [b"OK\n"], + ) self.mockserver.expect_exchange( - [b'idle "playlist"\n'], - [b'idle: playlist\n', b"OK\n"], - ) + [b'idle "playlist"\n'], + [b"idle: playlist\n", b"OK\n"], + ) self.mockserver.expect_exchange( - [b'noidle\n', b'currentsong\n'], - # it's a bit brief but it'll be accepted - [b"OK\n", ], - ) + [b"noidle\n", b"currentsong\n"], + # it's a bit brief but it'll be accepted + [ + b"OK\n", + ], + ) self.mockserver.expect_exchange( - [b'idle "playlist"\n'], - [b"ACK [4@0] I don't let you observe for no other reason\n"], - ) + [b'idle "playlist"\n'], + [b"ACK [4@0] I don't let you observe for no other reason\n"], + ) self.mockserver.expect_exchange( - [b'noidle\n', b'status\n'], - [b"ACK [4@0] whatever made the idle failed now fails too\n", ], - ) + [b"noidle\n", b"status\n"], + [ + b"ACK [4@0] whatever made the idle failed now fails too\n", + ], + ) # clearly longer than IMMEDIATE_COMMAND_TIMEOUT await asyncio.sleep(0.5) @@ -1550,7 +1593,9 @@ async def test_idle(self) -> None: except mpd.CommandError: pass else: - raise AssertionError("The status should have flushed out the error that made the idle fail in the first place") + raise AssertionError( + "The status should have flushed out the error that made the idle fail in the first place" + ) self.client.disconnect() @@ -1560,13 +1605,14 @@ async def test_idle(self) -> None: ) async def test_idle_timeout(self) -> None: await self.init_client() - self.mockserver.expect_exchange([b'currentsong\n'], [b"OK\n"]) - self.mockserver.expect_exchange([b'currentsong\n'], [b"OK\n"]) + self.mockserver.expect_exchange([b"currentsong\n"], [b"OK\n"]) + self.mockserver.expect_exchange([b"currentsong\n"], [b"OK\n"]) await self.client.currentsong() # pausing for exactly this duration triggers a special case in mpd.asyncio.MPDClient.__run await asyncio.sleep(self.client.IMMEDIATE_COMMAND_TIMEOUT) await self.client.currentsong() self.client.disconnect() + if __name__ == "__main__": unittest.main() diff --git a/mpd/twisted.py b/mpd/twisted.py index 3d74efc..48782e5 100644 --- a/mpd/twisted.py +++ b/mpd/twisted.py @@ -31,9 +31,19 @@ from twisted.internet import defer from twisted.protocols import basic -from mpd.base import (ERROR_PREFIX, HELLO_PREFIX, NEXT, SUCCESS, CommandError, - CommandListError, MPDClientBase, escape, logger, - mpd_command_provider, mpd_commands) +from mpd.base import ( + ERROR_PREFIX, + HELLO_PREFIX, + NEXT, + SUCCESS, + CommandError, + CommandListError, + MPDClientBase, + escape, + logger, + mpd_command_provider, + mpd_commands, +) def lock(func: Callable) -> Callable: diff --git a/pyproject.toml b/pyproject.toml index c474146..79023a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,3 +68,7 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = "mpd.tests.*" disable_error_code = ["attr-defined", "union-attr"] + +[tool.ruff] +target-version = "py310" +line-length = 88 From a3fbc0bfe93698f562ea594895de72122d66f12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 29 Sep 2025 14:50:48 +0200 Subject: [PATCH 3/6] add treefmt-nix --- flake.lock | 23 ++++++++++++++++++++++- flake.nix | 25 +++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 0603224..6d804db 100644 --- a/flake.lock +++ b/flake.lock @@ -39,7 +39,28 @@ "root": { "inputs": { "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1758728421, + "narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index ba5b501..1f51303 100644 --- a/flake.nix +++ b/flake.nix @@ -5,12 +5,31 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = inputs@{ flake-parts, ... }: + outputs = inputs@{ flake-parts, treefmt-nix, ... }: flake-parts.lib.mkFlake { inherit inputs; } ({ lib, ... }: { + imports = [ + treefmt-nix.flakeModule + ]; systems = lib.systems.flakeExposed; - perSystem = { pkgs, ... }: { + perSystem = { pkgs, config, ... }: { + treefmt = { + projectRootFile = "flake.nix"; + programs = { + ruff.format = true; + ruff.check = true; + mypy.enable = true; + mypy.directories = { + "." = { + modules = [ "mpd" ]; + }; + }; + }; + }; + devShells.default = pkgs.mkShell { packages = with pkgs; [ bashInteractive @@ -22,6 +41,8 @@ pypy3 twine mypy + ruff + config.treefmt.build.wrapper ]; }; }; From d40c0a6f701ad6b8e2d5a223fe9ac1565f069f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 29 Sep 2025 14:51:42 +0200 Subject: [PATCH 4/6] apply automatic ruff fixes --- doc/conf.py | 3 ++- examples/logger.py | 3 ++- mpd/base.py | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 3a32954..f0b2129 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -11,7 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/examples/logger.py b/examples/logger.py index 9bb560f..beb8249 100644 --- a/examples/logger.py +++ b/examples/logger.py @@ -1,4 +1,5 @@ -import logging, mpd +import logging +import mpd logging.basicConfig(level=logging.DEBUG) client = mpd.MPDClient() diff --git a/mpd/base.py b/mpd/base.py index 275fa9b..f2570fd 100644 --- a/mpd/base.py +++ b/mpd/base.py @@ -894,9 +894,9 @@ def command_list_end(self) -> Any: def add_command(cls, name: str, callback: Any) -> None: wrap_result = callback in cls._wrap_iterator_parsers if callback.mpd_commands_binary: - method = lambda self, *args: callback( - self, cls._execute_binary(self, name, args) - ) + + def method(self, *args): + return callback(self, cls._execute_binary(self, name, args)) else: method = _create_command(cls._execute, name, callback, wrap_result) # create new mpd commands as function: From ea0f6d5c480a1fc0e88635e8a777acc1ba9e811a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 29 Sep 2025 14:54:59 +0200 Subject: [PATCH 5/6] apply non-automatic ruff fixes --- examples/errorhandling.py | 2 +- examples/stickers.py | 11 +++++------ mpd/__init__.py | 20 ++++++++++---------- mpd/base.py | 10 ++++------ mpd/tests.py | 4 ++-- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/examples/errorhandling.py b/examples/errorhandling.py index a8c2add..27e6d6a 100644 --- a/examples/errorhandling.py +++ b/examples/errorhandling.py @@ -131,7 +131,7 @@ def main(): sys.exit(1) # Catch the remaining exit errors - except: + except Exception: sys.exit(0) diff --git a/examples/stickers.py b/examples/stickers.py index 94b3ca4..18e48e4 100644 --- a/examples/stickers.py +++ b/examples/stickers.py @@ -25,18 +25,17 @@ #! /usr/bin/env python -# Edit these -HOST = "localhost" -PORT = 6600 -PASS = None - - from optparse import OptionParser from socket import error as SocketError from sys import stderr from mpd import MPDClient, MPDError +# Edit these +HOST = "localhost" +PORT = 6600 +PASS = None + ACTIONS = ("get", "set", "delete", "list", "find") diff --git a/mpd/__init__.py b/mpd/__init__.py index e9697fa..3e4e035 100644 --- a/mpd/__init__.py +++ b/mpd/__init__.py @@ -17,16 +17,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with python-mpd2. If not, see . -from mpd.base import CommandError -from mpd.base import CommandListError -from mpd.base import ConnectionError -from mpd.base import FailureResponseCode -from mpd.base import IteratingError -from mpd.base import MPDClient -from mpd.base import MPDError -from mpd.base import PendingCommandError -from mpd.base import ProtocolError -from mpd.base import VERSION +from mpd.base import CommandError as CommandError +from mpd.base import CommandListError as CommandListError +from mpd.base import ConnectionError as ConnectionError +from mpd.base import FailureResponseCode as FailureResponseCode +from mpd.base import IteratingError as IteratingError +from mpd.base import MPDClient as MPDClient +from mpd.base import MPDError as MPDError +from mpd.base import PendingCommandError as PendingCommandError +from mpd.base import ProtocolError as ProtocolError +from mpd.base import VERSION as VERSION try: from mpd.twisted import MPDProtocol diff --git a/mpd/base.py b/mpd/base.py index f2570fd..56835a9 100644 --- a/mpd/base.py +++ b/mpd/base.py @@ -23,6 +23,7 @@ import sys import warnings from enum import Enum +from logging import NullHandler from typing import ( IO, Any, @@ -46,17 +47,14 @@ SUCCESS = "OK" NEXT = "list_OK" +logger = logging.getLogger(__name__) +logger.addHandler(NullHandler()) + def escape(text: str) -> str: return text.replace("\\", "\\\\").replace('"', '\\"') -from logging import NullHandler - -logger = logging.getLogger(__name__) -logger.addHandler(NullHandler()) - - # MPD Protocol errors as found in CommandError exceptions # https://github.com/MusicPlayerDaemon/MPD/blob/master/src/protocol/Ack.hxx class FailureResponseCode(Enum): diff --git a/mpd/tests.py b/mpd/tests.py index 6cbc73a..36c4193 100755 --- a/mpd/tests.py +++ b/mpd/tests.py @@ -1285,8 +1285,8 @@ def error(self, message: str) -> None: def _feed(self) -> None: if len(self._expectations[0][0]) == 0: _, response_lines = self._expectations.pop(0) - for l in response_lines: - self._output.put_nowait(l) + for line in response_lines: + self._output.put_nowait(line) def expect_exchange( self, request_lines: List[bytes], response_lines: List[bytes] From 6f6e5432694622a2e4c9cb831cff15d1e5c988f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 29 Sep 2025 14:59:09 +0200 Subject: [PATCH 6/6] only build on limited platforms --- flake.nix | 73 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/flake.nix b/flake.nix index 1f51303..ce8541d 100644 --- a/flake.nix +++ b/flake.nix @@ -9,42 +9,55 @@ treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = inputs@{ flake-parts, treefmt-nix, ... }: - flake-parts.lib.mkFlake { inherit inputs; } ({ lib, ... }: { + outputs = + inputs@{ flake-parts, treefmt-nix, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { imports = [ treefmt-nix.flakeModule ]; - systems = lib.systems.flakeExposed; - perSystem = { pkgs, config, ... }: { - treefmt = { - projectRootFile = "flake.nix"; - programs = { - ruff.format = true; - ruff.check = true; - mypy.enable = true; - mypy.directories = { - "." = { - modules = [ "mpd" ]; + systems = [ + "aarch64-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + perSystem = + { pkgs, config, ... }: + { + treefmt = { + projectRootFile = "flake.nix"; + programs = { + ruff.format = true; + ruff.check = true; + mypy.enable = true; + mypy.directories = { + "." = { + modules = [ "mpd" ]; + }; }; }; }; - }; - devShells.default = pkgs.mkShell { - packages = with pkgs; [ - bashInteractive - python310 - python311 - python312 - (python313.withPackages(ps: [ps.setuptools ps.tox ps.wheel ps.build])) - python314 - pypy3 - twine - mypy - ruff - config.treefmt.build.wrapper - ]; + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + bashInteractive + python310 + python311 + python312 + (python313.withPackages (ps: [ + ps.setuptools + ps.tox + ps.wheel + ps.build + ])) + python314 + pypy3 + twine + mypy + ruff + config.treefmt.build.wrapper + ]; + }; }; - }; - }); + }; }