diff --git a/discid/disc.py b/discid/disc.py index 2ddd7f1..f53ed88 100644 --- a/discid/disc.py +++ b/discid/disc.py @@ -31,11 +31,13 @@ FEATURES_IMPLEMENTED = list(_FEATURE_MAPPING.keys()) -def read(device=None, features=None): +def read( + device: str | bytes | None = None, features: list[str] | None = None +) -> "Disc": """Reads the TOC from the device given as string and returns a :class:`Disc` object. - That string can be either of :obj:`str ` or :obj:`bytes`. + The device string can be either of :obj:`str ` or :obj:`bytes`. However, it should in no case contain non-ASCII characters. If no device is given, a default, also given by :func:`get_default_device` is used. @@ -48,27 +50,31 @@ def read(device=None, features=None): A :exc:`DiscError` exception is raised when the reading fails, and :exc:`NotImplementedError` when libdiscid doesn't support reading discs on the current platform. + + :param device: the device name to use or :obj:`None` for using the default device + :param features: list of features to enable ("read" will always be assumed) """ disc = Disc() disc.read(device, features) return disc -def put(first, last, disc_sectors, track_offsets): +def put(first: int, last: int, disc_sectors: int, track_offsets: list[int]) -> "Disc": """Creates a TOC based on the information given and returns a :class:`Disc` object. - Takes the `first` track and `last` **audio** track as :obj:`int`. - `disc_sectors` is the end of the last audio track, - normally the total sector count of the disc. - `track_offsets` is a list of all audio track offsets. - Depending on how you get the total sector count, you might have to subtract 11400 (2:32 min.) for discs with data tracks. A :exc:`TOCError` exception is raised when illegal parameters are provided. + :param first: number of the first audio track + :param last: number of the last audio track + :param disc_sectors: the end of the last audio track, normally the total + sector count of the disc + :param track_offsets: list of all audio track offsets + .. seealso:: :musicbrainz:`Disc ID Calculation` """ disc = Disc() @@ -125,7 +131,9 @@ def _get_error_msg(self): except AttributeError: pass - def read(self, device=None, features=None): + def read( + self, device: str | bytes | None = None, features: list[str] | None = None + ) -> bool: """Reads the TOC from the device given as string The user is supposed to use :func:`discid.read`. @@ -162,7 +170,9 @@ def read(self, device=None, features=None): _LIB.discid_put.argtypes = (c_void_p, c_int, c_int, c_void_p) _LIB.discid_put.restype = c_int - def put(self, first, last, disc_sectors, track_offsets): + def put( + self, first: int, last: int, disc_sectors: int, track_offsets: list[int] + ) -> bool: """Creates a TOC based on the input given. The user is supposed to use :func:`discid.put`. @@ -282,21 +292,21 @@ def _get_mcn(self): return None @property - def id(self): + def id(self) -> str: """This is the MusicBrainz :musicbrainz:`Disc ID`, a :obj:`str ` object. """ return self._get_id() @property - def freedb_id(self): + def freedb_id(self) -> str: """This is the :musicbrainz:`FreeDB` Disc ID (without category), a :obj:`str ` object. """ return self._get_freedb_id() @property - def submission_url(self): + def submission_url(self) -> str | None: """Disc ID / TOC Submission URL for MusicBrainz With this url you can submit the current TOC @@ -313,7 +323,7 @@ def submission_url(self): return url @property - def toc_string(self): + def toc_string(self) -> str: """The TOC suitable as value of the `toc parameter` when accessing the MusicBrainz Web Service. @@ -337,17 +347,17 @@ def toc_string(self): return toc_string @property - def first_track_num(self): + def first_track_num(self) -> int: """Number of the first track""" return self._get_first_track_num() @property - def last_track_num(self): + def last_track_num(self) -> int: """Number of the last **audio** track""" return self._get_last_track_num() @property - def sectors(self): + def sectors(self) -> int: """Total length in sectors""" return self._get_sectors() @@ -355,15 +365,12 @@ def sectors(self): """This is an alias for :attr:`sectors`""" @property - def seconds(self): + def seconds(self) -> int: """Total length in seconds""" - if self.sectors is None: - return None - else: - return _sectors_to_seconds(self.sectors) + return _sectors_to_seconds(self.sectors) @property - def mcn(self): + def mcn(self) -> str | None: """This is the Media Catalogue Number (MCN/UPC/EAN) It is set after the `"mcn"` feature was requested on a read @@ -373,7 +380,7 @@ def mcn(self): return self._get_mcn() @property - def tracks(self): + def tracks(self) -> list[Track]: """A list of :class:`Track` objects for this Disc.""" tracks = [] assert self._success @@ -382,7 +389,7 @@ def tracks(self): return tracks @property - def cddb_query_string(self): + def cddb_query_string(self) -> str: """A CDDB query string suitable for querying CDDB servers. This is a :obj:`str ` object diff --git a/discid/libdiscid.py b/discid/libdiscid.py index 07feab3..09e6be0 100644 --- a/discid/libdiscid.py +++ b/discid/libdiscid.py @@ -132,7 +132,7 @@ def _get_version_string(): _LIB.discid_get_default_device.restype = c_char_p -def get_default_device(): +def get_default_device() -> str: """The default device to use for :func:`read` on this platform given as a :obj:`str ` object. """ diff --git a/discid/track.py b/discid/track.py index f01d903..08bb0a1 100644 --- a/discid/track.py +++ b/discid/track.py @@ -18,18 +18,26 @@ """Track class""" from ctypes import c_char_p, c_int, c_void_p +from typing import TYPE_CHECKING from discid.libdiscid import _LIB from discid.util import _decode, _sectors_to_seconds +if TYPE_CHECKING: + from .disc import Disc + class Track(object): - """Track objects are part of the :class:`Disc` class.""" + """Track objects are part of the :class:`Disc` class. + + :param disc: the :class:`Disc` object + :param number: the track number + """ - def __init__(self, disc, number): + def __init__(self, disc: "Disc", number: int): self._disc = disc self._number = number - assert self._disc._handle.value is not None + assert self._disc._handle and self._disc._handle.value is not None def __str__(self): assert self._disc._success @@ -68,17 +76,17 @@ def _get_track_isrc(self): return None @property - def number(self): + def number(self) -> int: """The track number""" return self._number @property - def offset(self): + def offset(self) -> int: """The track offset""" return self._get_track_offset() @property - def sectors(self): + def sectors(self) -> int: """The track length in sectors""" return self._get_track_length() @@ -86,15 +94,15 @@ def sectors(self): """This is an alias for :attr:`sectors`""" @property - def seconds(self): + def seconds(self) -> int: """Track length in seconds""" return _sectors_to_seconds(self.sectors) @property - def isrc(self): + def isrc(self) -> str | None: """The International Standard Recording Code - This will be `None` when the `"isrc"` feature was not requested + This will be :obj:`None` when the `"isrc"` feature was not requested or not supported, otherwise this is a :obj:`str ` object. """ return self._get_track_isrc() diff --git a/discid/util.py b/discid/util.py index 9325b35..4c1a275 100644 --- a/discid/util.py +++ b/discid/util.py @@ -22,7 +22,7 @@ SECTORS_PER_SECOND = 75 -def _encode(string: str | bytes): +def _encode(string: str | bytes) -> bytes: """Encode (unicode) string to byte string""" if isinstance(string, str): return string.encode() @@ -33,7 +33,7 @@ def _encode(string: str | bytes): # device names should be ASCII -def _decode(byte_string: bytes | str): +def _decode(byte_string: bytes | str) -> str: """Decode byte string to (unicode) string""" # this test for bytes works on Python 2 and 3 if isinstance(byte_string, bytes): diff --git a/doc/conf.py b/doc/conf.py index fe719bf..f626251 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -19,7 +19,7 @@ def __getattr__(cls, name): return Mock() -ctypes.cdll.LoadLibrary = Mock() +ctypes.cdll.LoadLibrary = Mock() # type: ignore # -- General configuration ----------------------------------------------------- @@ -27,13 +27,15 @@ def __getattr__(cls, name): extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.autosummary", "sphinx.ext.coverage", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", + "sphinx_autodoc_typehints", ] source_suffix = ".rst" master_doc = "index" -exclude_patterns = ["_build"] +exclude_patterns = ["_build", ".venv"] # General information about the project. project = "python-discid" diff --git a/pyproject.toml b/pyproject.toml index adc07a0..967daa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,9 @@ Changelog = "https://github.com/metabrainz/python-discid/blob/master/CHANGES.rst [dependency-groups] test = ["pytest"] docs = [ - "Sphinx~=8.1.3", - "sphinx-rtd-theme~=3.0.2", + "Sphinx>=8.1.3", + "sphinx-autodoc-typehints>=3.0.1", + "sphinx-rtd-theme>=3.0.2", ] dev = [ "pre-commit>=4.5.1", diff --git a/test_discid.py b/test_discid.py index 9974ce7..e88fac5 100755 --- a/test_discid.py +++ b/test_discid.py @@ -5,17 +5,30 @@ import math import os import unittest +from dataclasses import dataclass import discid import discid.util + +@dataclass +class DiscTestData: + name: str + first: int + last: int + sectors: int + offsets: list[int] + id: str + freedb: str + + test_discs = [ - { - "name": "Guano Apes - Don't give Me Names, without last data track", - "first": 1, - "last": 15, - "sectors": 258725, - "offsets": [ + DiscTestData( + name="Guano Apes - Don't give Me Names, without last data track", + first=1, + last=15, + sectors=258725, + offsets=[ 150, 17510, 33275, @@ -32,15 +45,15 @@ 215555, 235590, ], - "id": "TqvKjMu7dMliSfmVEBtrL7sBSno-", - "freedb": "b60d770f", - }, - { - "name": "Lunar - There Is No 1, first track is 2", - "first": 2, - "last": 11, - "sectors": 225781, - "offsets": [ + id="TqvKjMu7dMliSfmVEBtrL7sBSno-", + freedb="b60d770f", + ), + DiscTestData( + name="Lunar - There Is No 1, first track is 2", + first=2, + last=11, + sectors=225781, + offsets=[ 150, 11512, 34143, @@ -52,9 +65,9 @@ 195438, 201127, ], - "id": "6RDuz0d7.M5SVMLe1z4DP0yaEC8-", - "freedb": "840bc20b", - }, + id="6RDuz0d7.M5SVMLe1z4DP0yaEC8-", + freedb="840bc20b", + ), ] @@ -124,18 +137,18 @@ def test_put_fail(self): def test_put_success(self): for test_disc in test_discs: disc = discid.put( - test_disc["first"], - test_disc["last"], - test_disc["sectors"], - test_disc["offsets"], + test_disc.first, + test_disc.last, + test_disc.sectors, + test_disc.offsets, ) - self.assertEqual(disc.id, test_disc["id"]) - self.assertEqual(disc.freedb_id, test_disc["freedb"]) - self.assertEqual(disc.first_track_num, test_disc["first"]) - self.assertEqual(disc.last_track_num, test_disc["last"]) - self.assertEqual(disc.sectors, test_disc["sectors"]) + self.assertEqual(disc.id, test_disc.id) + self.assertEqual(disc.freedb_id, test_disc.freedb) + self.assertEqual(disc.first_track_num, test_disc.first) + self.assertEqual(disc.last_track_num, test_disc.last) + self.assertEqual(disc.sectors, test_disc.sectors) track_offsets = [track.offset for track in disc.tracks] - self.assertEqual(track_offsets, test_disc["offsets"]) + self.assertEqual(track_offsets, test_disc.offsets) self.assertEqual( disc.sectors, disc.tracks[-1].offset + disc.tracks[-1].sectors ) @@ -147,7 +160,7 @@ def test_put_success(self): ) self.assertEqual(type(track.seconds), int) toc_string = [ - test_disc["first"], + test_disc.first, disc.last_track_num, disc.sectors, ] + track_offsets @@ -255,10 +268,10 @@ def test_read_put(self): disc = discid.read(features=["mcn", "isrc"]) # read from default drive test_disc = test_discs[0] disc = discid.put( - test_disc["first"], - test_disc["last"], - test_disc["sectors"], - test_disc["offsets"], + test_disc.first, + test_disc.last, + test_disc.sectors, + test_disc.offsets, ) self.assertTrue(disc.mcn is None) for track in disc.tracks: