diff --git a/beets/library.py b/beets/library.py index 888836cd9..3cdc713bb 100644 --- a/beets/library.py +++ b/beets/library.py @@ -53,8 +53,9 @@ class PathQuery(dbcore.FieldQuery): """ def __init__(self, field, pattern, fast=True, case_sensitive=None): - """Create a path query. `pattern` must be a path, either to a - file or a directory. + """Create a path query. + + `pattern` must be a path, either to a file or a directory. `case_sensitive` can be a bool or `None`, indicating that the behavior should depend on the filesystem. @@ -140,8 +141,10 @@ class DateType(types.Float): class PathType(types.Type): - """A dbcore type for filesystem paths. These are represented as - `bytes` objects, in keeping with the Unix filesystem abstraction. + """A dbcore type for filesystem paths. + + These are represented as `bytes` objects, in keeping with + the Unix filesystem abstraction. """ sql = 'BLOB' @@ -149,8 +152,9 @@ class PathType(types.Type): model_type = bytes def __init__(self, nullable=False): - """Create a path type object. `nullable` controls whether the - type may be missing, i.e., None. + """Create a path type object. + + `nullable` controls whether the type may be missing, i.e., None. """ self.nullable = nullable @@ -282,7 +286,8 @@ PF_KEY_DEFAULT = 'default' # Exceptions. class FileOperationError(Exception): - """Indicates an error when interacting with a file on disk. + """Indicate an error when interacting with a file on disk. + Possibilities include an unsupported media type, a permissions error, and an unhandled Mutagen exception. """ @@ -296,8 +301,10 @@ class FileOperationError(Exception): self.reason = reason def text(self): - """Get a string representing the error. Describes both the - underlying reason and the file path in question. + """Get a string representing the error. + + Describe both the underlying reason and the file path + in question. """ return '{}: {}'.format( util.displayable_path(self.path), @@ -310,16 +317,14 @@ class FileOperationError(Exception): class ReadError(FileOperationError): - """An error while reading a file (i.e. in `Item.read`). - """ + """An error while reading a file (i.e. in `Item.read`).""" def __str__(self): return 'error reading ' + super().text() class WriteError(FileOperationError): - """An error while writing a file (i.e. in `Item.write`). - """ + """An error while writing a file (i.e. in `Item.write`).""" def __str__(self): return 'error writing ' + super().text() @@ -328,12 +333,10 @@ class WriteError(FileOperationError): # Item and Album model classes. class LibModel(dbcore.Model): - """Shared concrete functionality for Items and Albums. - """ + """Shared concrete functionality for Items and Albums.""" + # Config key that specifies how an instance should be formatted. _format_config_key = None - """Config key that specifies how an instance should be formatted. - """ def _template_funcs(self): funcs = DefaultTemplateFunctions(self, self._db).functions() @@ -410,6 +413,7 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): def _get(self, key): """Get the value for a key, either from the album or the item. + Raise a KeyError for invalid keys. """ if self.for_path and key in self.album_keys: @@ -422,8 +426,10 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): raise KeyError(key) def __getitem__(self, key): - """Get the value for a key. `artist` and `albumartist` - are fallback values for each other when not set. + """Get the value for a key. + + `artist` and `albumartist` are fallback values for each other + when not set. """ value = self._get(key) @@ -448,6 +454,7 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): class Item(LibModel): + """Represent a song or track.""" _table = 'items' _flex_table = 'item_attributes' _fields = { @@ -539,22 +546,18 @@ class Item(LibModel): 'data_source': types.STRING, } + # Set of item fields that are backed by `MediaFile` fields. + # Any kind of field (fixed, flexible, and computed) may be a media + # field. Only these fields are read from disk in `read` and written in + # `write`. _media_fields = set(MediaFile.readable_fields()) \ .intersection(_fields.keys()) - """Set of item fields that are backed by `MediaFile` fields. - - Any kind of field (fixed, flexible, and computed) may be a media - field. Only these fields are read from disk in `read` and written in - `write`. - """ + # Set of item fields that are backed by *writable* `MediaFile` tag + # fields. + # This excludes fields that represent audio data, such as `bitrate` or + # `length`. _media_tag_fields = set(MediaFile.fields()).intersection(_fields.keys()) - """Set of item fields that are backed by *writable* `MediaFile` tag - fields. - - This excludes fields that represent audio data, such as `bitrate` or - `length`. - """ _formatter = FormattedItemMapping @@ -562,8 +565,8 @@ class Item(LibModel): _format_config_key = 'format_item' + # Cached album object. Read-only. __album = None - """Cached album object. Read-only.""" @property def _cached_album(self): @@ -594,8 +597,7 @@ class Item(LibModel): @classmethod def from_path(cls, path): - """Creates a new item from the media file at the specified path. - """ + """Create a new item from the media file at the specified path.""" # Initiate with values that aren't read from files. i = cls(album_id=None) i.read(path) @@ -603,8 +605,7 @@ class Item(LibModel): return i def __setitem__(self, key, value): - """Set the item's value for a standard field or a flexattr. - """ + """Set the item's value for a standard field or a flexattr.""" # Encode unicode paths and read buffers. if key == 'path': if isinstance(value, str): @@ -621,7 +622,9 @@ class Item(LibModel): def __getitem__(self, key): """Get the value for a field, falling back to the album if - necessary. Raise a KeyError if the field is not available. + necessary. + + Raise a KeyError if the field is not available. """ try: return super().__getitem__(key) @@ -641,8 +644,9 @@ class Item(LibModel): ) def keys(self, computed=False, with_album=True): - """Get a list of available field names. `with_album` - controls whether the album's fields are included. + """Get a list of available field names. + + `with_album` controls whether the album's fields are included. """ keys = super().keys(computed=computed) if with_album and self._cached_album: @@ -653,7 +657,9 @@ class Item(LibModel): def get(self, key, default=None, with_album=True): """Get the value for a given key or `default` if it does not - exist. Set `with_album` to false to skip album fallback. + exist. + + Set `with_album` to false to skip album fallback. """ try: return self._get(key, default, raise_=with_album) @@ -663,8 +669,9 @@ class Item(LibModel): return default def update(self, values): - """Set all key/value pairs in the mapping. If mtime is - specified, it is not reset (as it might otherwise be). + """Set all key/value pairs in the mapping. + + If mtime is specified, it is not reset (as it might otherwise be). """ super().update(values) if self.mtime == 0 and 'mtime' in values: @@ -690,10 +697,10 @@ class Item(LibModel): """Read the metadata from the associated file. If `read_path` is specified, read metadata from that file - instead. Updates all the properties in `_media_fields` + instead. Update all the properties in `_media_fields` from the media file. - Raises a `ReadError` if the file could not be read. + Raise a `ReadError` if the file could not be read. """ if read_path is None: read_path = self.path @@ -769,10 +776,10 @@ class Item(LibModel): plugins.send('after_write', item=self, path=path) def try_write(self, *args, **kwargs): - """Calls `write()` but catches and logs `FileOperationError` + """Call `write()` but catch and log `FileOperationError` exceptions. - Returns `False` an exception was caught and `True` otherwise. + Return `False` an exception was caught and `True` otherwise. """ try: self.write(*args, **kwargs) @@ -782,7 +789,7 @@ class Item(LibModel): return False def try_sync(self, write, move, with_album=True): - """Synchronize the item with the database and, possibly, updates its + """Synchronize the item with the database and, possibly, update its tags on disk and its path (by moving the file). `write` indicates whether to write new tags into the file. Similarly, @@ -806,7 +813,7 @@ class Item(LibModel): # Files themselves. def move_file(self, dest, operation=MoveOperation.MOVE): - """Move, copy, link or hardlink the item's depending on `operation`, + """Move, copy, link or hardlink the item depending on `operation`, updating the path value if the move succeeds. If a file exists at `dest`, then it is slightly modified to be unique. @@ -848,7 +855,7 @@ class Item(LibModel): self.path = dest def current_mtime(self): - """Returns the current mtime of the file, rounded to the nearest + """Return the current mtime of the file, rounded to the nearest integer. """ return int(os.path.getmtime(syspath(self.path))) @@ -867,9 +874,12 @@ class Item(LibModel): # Model methods. def remove(self, delete=False, with_album=True): - """Removes the item. If `delete`, then the associated file is - removed from disk. If `with_album`, then the item's album (if - any) is removed if it the item was the last in the album. + """Remove the item. + + If `delete`, then the associated file is removed from disk. + + If `with_album`, then the item's album (if any) is removed + if the item was the last in the album. """ super().remove() @@ -892,9 +902,10 @@ class Item(LibModel): def move(self, operation=MoveOperation.MOVE, basedir=None, with_album=True, store=True): """Move the item to its designated location within the library - directory (provided by destination()). Subdirectories are - created as needed. If the operation succeeds, the item's path - field is updated to reflect the new location. + directory (provided by destination()). + + Subdirectories are created as needed. If the operation succeeds, + the item's path field is updated to reflect the new location. Instead of moving the item it can also be copied, linked or hardlinked depending on `operation` which should be an instance of @@ -908,8 +919,8 @@ class Item(LibModel): By default, the item is stored to the database if it is in the database, so any dirty fields prior to the move() call will be written as a side effect. - If `store` is `False` however, the item won't be stored and you'll - have to manually store it after invoking this method. + If `store` is `False` however, the item won't be stored and it will + have to be manually stored after invoking this method. """ self._check_db() dest = self.destination(basedir=basedir) @@ -939,12 +950,13 @@ class Item(LibModel): def destination(self, fragment=False, basedir=None, platform=None, path_formats=None, replacements=None): - """Returns the path in the library directory designated for the - item (i.e., where the file ought to be). fragment makes this - method return just the path fragment underneath the root library - directory; the path is also returned as Unicode instead of - encoded as a bytestring. basedir can override the library's base - directory for the destination. + """Return the path in the library directory designated for the + item (i.e., where the file ought to be). + + fragment makes this method return just the path fragment underneath + the root library directory; the path is also returned as Unicode + instead of encoded as a bytestring. basedir can override the library's + base directory for the destination. """ self._check_db() platform = platform or sys.platform @@ -1016,9 +1028,10 @@ class Item(LibModel): class Album(LibModel): - """Provides access to information about albums stored in a - library. Reflects the library's "albums" table, including album - art. + """Provide access to information about albums stored in a + library. + + Reflects the library's "albums" table, including album art. """ _table = 'albums' _flex_table = 'album_attributes' @@ -1076,6 +1089,7 @@ class Album(LibModel): 'artist': SmartArtistSort, } + # List of keys that are set on an album's items. item_keys = [ 'added', 'albumartist', @@ -1113,8 +1127,6 @@ class Album(LibModel): 'original_month', 'original_day', ] - """List of keys that are set on an album's items. - """ _format_config_key = 'format_album' @@ -1128,16 +1140,19 @@ class Album(LibModel): return getters def items(self): - """Returns an iterable over the items associated with this + """Return an iterable over the items associated with this album. """ return self._db.items(dbcore.MatchQuery('album_id', self.id)) def remove(self, delete=False, with_items=True): - """Removes this album and all its associated items from the - library. If delete, then the items' files are also deleted - from disk, along with any album art. The directories - containing the album are also removed (recursively) if empty. + """Remove this album and all its associated items from the + library. + + If delete, then the items' files are also deleted from disk, + along with any album art. The directories containing the album are + also removed (recursively) if empty. + Set with_items to False to avoid removing the album's items. """ super().remove() @@ -1208,8 +1223,8 @@ class Album(LibModel): By default, the album is stored to the database, persisting any modifications to its metadata. If `store` is `False` however, - the album is not stored automatically, and you'll have to manually - store it after invoking this method. + the album is not stored automatically, and it will have to be manually + stored after invoking this method. """ basedir = basedir or self._db.directory @@ -1230,7 +1245,7 @@ class Album(LibModel): self.store() def item_dir(self): - """Returns the directory containing the album's first item, + """Return the directory containing the album's first item, provided that such an item exists. """ item = self.items().get() @@ -1239,8 +1254,7 @@ class Album(LibModel): return os.path.dirname(item.path) def _albumtotal(self): - """Return the total number of tracks on all discs on the album - """ + """Return the total number of tracks on all discs on the album.""" if self.disctotal == 1 or not beets.config['per_disc_numbering']: return self.items()[0].tracktotal @@ -1260,8 +1274,10 @@ class Album(LibModel): return total def art_destination(self, image, item_dir=None): - """Returns a path to the destination for the album art image - for the album. `image` is the path of the image that will be + """Return a path to the destination for the album art image + for the album. + + `image` is the path of the image that will be moved there (used for its extension). The path construction uses the existing path of the album's @@ -1289,11 +1305,12 @@ class Album(LibModel): return bytestring_path(dest) def set_art(self, path, copy=True): - """Sets the album's cover art to the image at the given path. + """Set the album's cover art to the image at the given path. + The image is copied (or moved) into place, replacing any existing art. - Sends an 'art_set' event with `self` as the sole argument. + Send an 'art_set' event with `self` as the sole argument. """ path = bytestring_path(path) oldart = self.artpath @@ -1320,10 +1337,12 @@ class Album(LibModel): plugins.send('art_set', album=self) def store(self, fields=None): - """Update the database with the album information. The album's - tracks are also updated. - :param fields: The fields to be stored. If not specified, all fields - will be. + """Update the database with the album information. + + The album's tracks are also updated. + + `fields` represents the fields to be stored. If not specified, + all fields will be. """ # Get modified track fields. track_updates = {} @@ -1408,10 +1427,11 @@ def parse_query_string(s, model_cls): def _sqlite_bytelower(bytestring): """ A custom ``bytelower`` sqlite function so we can compare - bytestrings in a semi case insensitive fashion. This is to work - around sqlite builds are that compiled with - ``-DSQLITE_LIKE_DOESNT_MATCH_BLOBS``. See - ``https://github.com/beetbox/beets/issues/2172`` for details. + bytestrings in a semi case insensitive fashion. + + This is to work around sqlite builds are that compiled with + ``-DSQLITE_LIKE_DOESNT_MATCH_BLOBS``. See + ``https://github.com/beetbox/beets/issues/2172`` for details. """ return bytestring.lower() @@ -1419,8 +1439,7 @@ def _sqlite_bytelower(bytestring): # The Library: interface to the database. class Library(dbcore.Database): - """A database of music containing songs and albums. - """ + """A database of music containing songs and albums.""" _models = (Item, Album) def __init__(self, path='library.blb', @@ -1446,7 +1465,9 @@ class Library(dbcore.Database): def add(self, obj): """Add the :class:`Item` or :class:`Album` object to the library - database. Return the object's new id. + database. + + Return the object's new id. """ obj.add(self) self._memotable = {} @@ -1482,8 +1503,10 @@ class Library(dbcore.Database): # Querying. def _fetch(self, model_cls, query, sort=None): - """Parse a query and fetch. If a order specification is present - in the query string the `sort` argument is ignored. + """Parse a query and fetch. + + If an order specification is present in the query string + the `sort` argument is ignored. """ # Parse the query, if necessary. try: @@ -1506,40 +1529,38 @@ class Library(dbcore.Database): @staticmethod def get_default_album_sort(): - """Get a :class:`Sort` object for albums from the config option. - """ + """Get a :class:`Sort` object for albums from the config option.""" return dbcore.sort_from_strings( Album, beets.config['sort_album'].as_str_seq()) @staticmethod def get_default_item_sort(): - """Get a :class:`Sort` object for items from the config option. - """ + """Get a :class:`Sort` object for items from the config option.""" return dbcore.sort_from_strings( Item, beets.config['sort_item'].as_str_seq()) def albums(self, query=None, sort=None): - """Get :class:`Album` objects matching the query. - """ + """Get :class:`Album` objects matching the query.""" return self._fetch(Album, query, sort or self.get_default_album_sort()) def items(self, query=None, sort=None): - """Get :class:`Item` objects matching the query. - """ + """Get :class:`Item` objects matching the query.""" return self._fetch(Item, query, sort or self.get_default_item_sort()) # Convenience accessors. def get_item(self, id): - """Fetch an :class:`Item` by its ID. Returns `None` if no match is - found. + """Fetch a :class:`Item` by its ID. + + Return `None` if no match is found. """ return self._get(Item, id) def get_album(self, item_or_id): """Given an album ID or an item associated with an album, return - an :class:`Album` object for the album. If no such album exists, - returns `None`. + a :class:`Album` object for the album. + + If no such album exists, return `None`. """ if isinstance(item_or_id, int): album_id = item_or_id @@ -1554,30 +1575,37 @@ class Library(dbcore.Database): def _int_arg(s): """Convert a string argument to an integer for use in a template - function. May raise a ValueError. + function. + + May raise a ValueError. """ return int(s.strip()) class DefaultTemplateFunctions: """A container class for the default functions provided to path - templates. These functions are contained in an object to provide + templates. + + These functions are contained in an object to provide additional context to the functions -- specifically, the Item being evaluated. """ _prefix = 'tmpl_' def __init__(self, item=None, lib=None): - """Parametrize the functions. If `item` or `lib` is None, then - some functions (namely, ``aunique``) will always evaluate to the - empty string. + """Parametrize the functions. + + If `item` or `lib` is None, then some functions (namely, ``aunique``) + will always evaluate to the empty string. """ self.item = item self.lib = lib def functions(self): - """Returns a dictionary containing the functions defined in this - object. The keys are function names (as exposed in templates) + """Return a dictionary containing the functions defined in this + object. + + The keys are function names (as exposed in templates) and the values are Python functions. """ out = {} @@ -1592,7 +1620,7 @@ class DefaultTemplateFunctions: @staticmethod def tmpl_upper(s): - """Covert a string to upper case.""" + """Convert a string to upper case.""" return s.upper() @staticmethod @@ -1630,21 +1658,20 @@ class DefaultTemplateFunctions: @staticmethod def tmpl_asciify(s): - """Translate non-ASCII characters to their ASCII equivalents. - """ + """Translate non-ASCII characters to their ASCII equivalents.""" return util.asciify_path(s, beets.config['path_sep_replace'].as_str()) @staticmethod def tmpl_time(s, fmt): - """Format a time value using `strftime`. - """ + """Format a time value using `strftime`.""" cur_fmt = beets.config['time_format'].as_str() return time.strftime(fmt, time.strptime(s, cur_fmt)) def tmpl_aunique(self, keys=None, disam=None, bracket=None): """Generate a string that is guaranteed to be unique among all - albums in the library who share the same set of keys. A fields - from "disam" is used in the string if one is sufficient to + albums in the library who share the same set of keys. + + A fields from "disam" is used in the string if one is sufficient to disambiguate the albums. Otherwise, a fallback opaque value is used. Both "keys" and "disam" should be given as whitespace-separated lists of field names, while "bracket" is a @@ -1735,14 +1762,15 @@ class DefaultTemplateFunctions: @staticmethod def tmpl_first(s, count=1, skip=0, sep='; ', join_str='; '): - """ Gets the item(s) from x to y in a string separated by something - and join then with something + """Get the item(s) from x to y in a string separated by something + and join then with something. - :param s: the string - :param count: The number of items included - :param skip: The number of items skipped - :param sep: the separator. Usually is '; ' (default) or '/ ' - :param join_str: the string which will join the items, default '; '. + Args: + s: the string + count: The number of items included + skip: The number of items skipped + sep: the separator. Usually is '; ' (default) or '/ ' + join_str: the string which will join the items, default '; '. """ skip = int(skip) count = skip + int(count) @@ -1752,10 +1780,13 @@ class DefaultTemplateFunctions: """ If field exists return trueval or the field (default) otherwise, emit return falseval (if provided). - :param field: The name of the field - :param trueval: The string if the condition is true - :param falseval: The string if the condition is false - :return: The string, based on condition + Args: + field: The name of the field + trueval: The string if the condition is true + falseval: The string if the condition is false + + Returns: + The string, based on condition. """ if field in self.item: return trueval if trueval else self.item.formatted().get(field)