1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-09-18 18:31:04 +00:00

Dropping Python 2 support

This commit is contained in:
Alban Féron 2019-12-23 16:23:57 +01:00
parent 2f9fa0da6f
commit 1d01450f33
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
31 changed files with 41 additions and 176 deletions

View File

@ -1,7 +1,6 @@
dist: xenial
language: python
python:
- 2.7
- 3.5
- 3.6
- 3.7

View File

@ -4,7 +4,7 @@ _Supysonic_ is a Python implementation of the [Subsonic][] server API.
[![Build Status](https://travis-ci.org/spl0k/supysonic.svg?branch=master)](https://travis-ci.org/spl0k/supysonic)
[![codecov](https://codecov.io/gh/spl0k/supysonic/branch/master/graph/badge.svg)](https://codecov.io/gh/spl0k/supysonic)
![Python](https://img.shields.io/badge/python-2.7%2C%203.5%2C%203.6%2C%203.7-blue.svg)
![Python](https://img.shields.io/badge/python-3.5%2C%203.6%2C%203.7-blue.svg)
Current supported features are:
* browsing (by folders or tags)
@ -51,20 +51,20 @@ or
$ pip install .
but not both. Please note that the `pip` method doesn't seem to work with
Python 2.7.
but not both.
### Prerequisites
You'll need these to run _Supysonic_:
* Python 2.7 or >= 3.5
* Python >= 3.5
* [Flask](http://flask.pocoo.org/)
* [PonyORM](https://ponyorm.com/)
* [Python Imaging Library](https://github.com/python-pillow/Pillow)
* [requests](http://docs.python-requests.org/)
* [mutagen](https://mutagen.readthedocs.io/en/latest/)
* [watchdog](https://github.com/gorakhargosh/watchdog)
* [zipstream](https://github.com/allanlei/python-zipstream)
All the dependencies will automatically be installed by the
installation command above.
@ -142,7 +142,7 @@ _Supysonic_ can run as a WSGI application with the `cgi-bin/supysonic.wsgi`
file. To run it within an _Apache2_ server, first you need to install the WSGI
module and enable it.
$ apt install libapache2-mod-wsgi
$ apt install libapache2-mod-wsgi-py3
$ a2enmod wsgi
Next, edit the _Apache_ configuration to load the application. Here's a basic

View File

@ -20,7 +20,6 @@ reqs = [
"Pillow",
"requests>=1.0.0",
"mutagen>=1.33",
"scandir<2.0.0; python_version <= '2.7'",
"watchdog>=0.8.0",
"zipstream",
]
@ -55,8 +54,6 @@ setup(
"Intended Audience :: End Users/Desktop",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",

View File

@ -18,7 +18,6 @@ from pony.orm import ObjectNotFound
from pony.orm import commit
from ..managers.user import UserManager
from ..py23 import dict
from .exceptions import Unauthorized
from .formatters import JSONFormatter, JSONPFormatter, XMLFormatter

View File

@ -24,7 +24,6 @@ from ..db import (
User,
)
from ..db import now
from ..py23 import dict
from . import api
from .exceptions import GenericError, NotFound

View File

@ -19,7 +19,6 @@ from ..db import Track, Album, Artist, Folder, User
from ..db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder
from ..db import RatingTrack, RatingFolder
from ..lastfm import LastFm
from ..py23 import dict
from . import api, get_entity, get_entity_id
from .exceptions import AggregateException, GenericError, MissingParameter, NotFound
@ -150,7 +149,9 @@ def rate():
if rating == 0:
if tid is not None:
delete(
r for r in RatingTrack if r.user.id == request.user.id and r.rated.id == tid
r
for r in RatingTrack
if r.user.id == request.user.id and r.rated.id == tid
)
else:
delete(

View File

@ -14,7 +14,6 @@ from flask import request
from pony.orm import ObjectNotFound, select
from ..db import Folder, Artist, Album, Track
from ..py23 import dict
from . import api, get_entity, get_entity_id

View File

@ -10,7 +10,6 @@
from flask import request
from ..db import ChatMessage, User
from ..py23 import dict
from . import api

View File

@ -10,7 +10,6 @@
from flask import json, jsonify, make_response
from xml.etree import ElementTree
from ..py23 import dict, strtype
from . import API_VERSION
@ -103,7 +102,7 @@ class XMLFormatter(BaseFormatter):
"""
if not isinstance(dictionary, dict):
raise TypeError("Expecting a dict")
if not all(map(lambda x: isinstance(x, strtype), dictionary)):
if not all(map(lambda x: isinstance(x, str), dictionary)):
raise TypeError("Dictionary keys must be strings")
for name, value in dictionary.items():
@ -125,7 +124,7 @@ class XMLFormatter(BaseFormatter):
def __value_tostring(self, value):
if value is None:
return None
if isinstance(value, strtype):
if isinstance(value, str):
return value
if isinstance(value, bool):
return str(value).lower()

View File

@ -8,7 +8,6 @@
#
# Distributed under terms of the GNU AGPLv3 license.
import codecs
import logging
import mimetypes
import os.path
@ -33,7 +32,6 @@ from .. import scanner
from ..cache import CacheMiss
from ..covers import get_embedded_cover
from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now
from ..py23 import dict
from . import api, get_entity, get_entity_id
from .exceptions import (
@ -137,9 +135,7 @@ def stream_media():
raise GenericError(message)
transcoder, decoder, encoder = [
prepare_transcoding_cmdline(
x, res, src_suffix, dst_suffix, dst_bitrate
)
prepare_transcoding_cmdline(x, res, src_suffix, dst_suffix, dst_bitrate)
for x in (transcoder, decoder, encoder)
]
try:
@ -304,7 +300,8 @@ def lyrics():
logger.debug("Found lyrics file: " + lyrics_path)
try:
lyrics = read_file_as_unicode(lyrics_path)
with open(lyrics_path, "rt") as f:
lyrics = f.read()
except UnicodeError:
# Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or
# return no lyrics. Log it anyway.
@ -350,22 +347,3 @@ def lyrics():
logger.warning("Error while requesting the ChartLyrics API: " + str(e))
return request.formatter("lyrics", lyrics)
def read_file_as_unicode(path):
""" Opens a file trying with different encodings and returns the contents as a unicode string """
encodings = ["utf-8", "latin1"] # Should be extended to support more encodings
for enc in encodings:
try:
contents = codecs.open(path, "r", encoding=enc).read()
logger.debug("Read file {} with {} encoding".format(path, enc))
# Maybe save the encoding somewhere to prevent going through this loop each time for the same file
return contents
except UnicodeError:
pass
# Fallback to ASCII
logger.debug("Reading file {} with ascii encoding".format(path))
return unicode(open(path, "r").read())

View File

@ -12,7 +12,6 @@ import uuid
from flask import request
from ..db import Playlist, User, Track
from ..py23 import dict
from . import api, get_entity
from .exceptions import Forbidden, MissingParameter, NotFound

View File

@ -13,7 +13,6 @@ from flask import request
from pony.orm import select
from ..db import Folder, Track, Artist, Album
from ..py23 import dict
from . import api
from .exceptions import MissingParameter

View File

@ -9,7 +9,6 @@
from flask import request
from ..py23 import dict
from . import api

View File

@ -12,7 +12,6 @@ from functools import wraps
from ..db import User
from ..managers.user import UserManager
from ..py23 import dict
from . import api, decode_password
from .exceptions import Forbidden, GenericError, NotFound

View File

@ -18,8 +18,6 @@ import tempfile
import threading
from time import time
from .py23 import scandir, osreplace
logger = logging.getLogger(__name__)
@ -80,7 +78,7 @@ class Cache(object):
for mtime, size, key in sorted(
[
(f.stat().st_mtime, f.stat().st_size, f.name)
for f in scandir(self._cache_dir)
for f in os.scandir(self._cache_dir)
if f.is_file()
]
):
@ -158,7 +156,7 @@ class Cache(object):
with self._lock:
if self._auto_prune:
self._make_space(size, key=key)
osreplace(f.name, self._filepath(key))
os.replace(f.name, self._filepath(key))
self._record_file(key, size)
except OSError as e:
# Ignore error from trying to delete the renamed temp file

View File

@ -8,14 +8,11 @@
#
# Distributed under terms of the GNU AGPLv3 license.
try:
from configparser import RawConfigParser
except ImportError:
from ConfigParser import RawConfigParser
import os
import tempfile
from configparser import RawConfigParser
current_config = None
@ -92,11 +89,6 @@ class IniConfig(DefaultConfig):
return True
if lv in ("no", "false", "off"):
return False
try:
if isinstance(value, unicode):
return str(value)
except NameError:
pass
return value
@classmethod

View File

@ -16,8 +16,8 @@ from mutagen.easyid3 import EasyID3
from mutagen.flac import FLAC, Picture
from mutagen._vorbis import VCommentDict
from PIL import Image
from os import scandir
from .py23 import scandir, strtype
EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp")
NAMING_SCORE_RULES = (
@ -90,7 +90,7 @@ def find_cover_in_folder(path, album_name=None):
def get_embedded_cover(path):
if not isinstance(path, strtype): # pragma: nocover
if not isinstance(path, str): # pragma: nocover
raise TypeError("Expecting string, got " + str(type(path)))
if not os.path.exists(path):

View File

@ -11,7 +11,6 @@ from multiprocessing.connection import Client
from .exceptions import DaemonUnavailableError
from ..config import get_current_config
from ..py23 import strtype
from ..utils import get_secret_key
__all__ = ["DaemonClient"]
@ -86,13 +85,13 @@ class DaemonClient(object):
)
def add_watched_folder(self, folder):
if not isinstance(folder, strtype):
if not isinstance(folder, str):
raise TypeError("Expecting string, got " + str(type(folder)))
with self.__get_connection() as c:
c.send(AddWatchedFolderCommand(folder))
def remove_watched_folder(self, folder):
if not isinstance(folder, strtype):
if not isinstance(folder, str):
raise TypeError("Expecting string, got " + str(type(folder)))
with self.__get_connection() as c:
c.send(RemoveWatchedFolder(folder))

View File

@ -20,15 +20,9 @@ from pony.orm import ObjectNotFound, DatabaseError
from pony.orm import buffer
from pony.orm import min, max, avg, sum, exists
from pony.orm import db_session
from urllib.parse import urlparse, parse_qsl
from uuid import UUID, uuid4
from .py23 import dict, strtype
try:
from urllib.parse import urlparse, parse_qsl
except ImportError:
from urlparse import urlparse, parse_qsl
SCHEMA_VERSION = "20190915"
@ -538,7 +532,7 @@ class Playlist(db.Entity):
tid = track
elif isinstance(track, Track):
tid = track.id
elif isinstance(track, strtype):
elif isinstance(track, str):
tid = UUID(track)
if self.tracks and len(self.tracks) > 0:
@ -557,7 +551,7 @@ class Playlist(db.Entity):
def parse_uri(database_uri):
if not isinstance(database_uri, strtype):
if not isinstance(database_uri, str):
raise TypeError("Expecting a string")
uri = urlparse(database_uri)

View File

@ -17,7 +17,6 @@ from pony.orm import ObjectNotFound
from ..db import User, ClientPrefs
from ..lastfm import LastFm
from ..managers.user import UserManager
from ..py23 import dict
from . import admin_only, frontend

View File

@ -11,8 +11,6 @@ import hashlib
import logging
import requests
from .py23 import strtype
logger = logging.getLogger(__name__)
@ -87,7 +85,7 @@ class LastFm:
sig_str = b""
for k, v in sorted(kwargs.items()):
k = k.encode("utf-8")
v = v.encode("utf-8") if isinstance(v, strtype) else str(v).encode("utf-8")
v = v.encode("utf-8") if isinstance(v, str) else str(v).encode("utf-8")
sig_str += k + v
sig = hashlib.md5(sig_str + self.__api_secret).hexdigest()

View File

@ -16,7 +16,6 @@ from pony.orm import ObjectNotFound
from ..daemon.client import DaemonClient
from ..daemon.exceptions import DaemonUnavailableError
from ..db import Folder, Track, Artist, Album, User, RatingTrack, StarredTrack
from ..py23 import strtype
class FolderManager:

View File

@ -16,7 +16,6 @@ import uuid
from pony.orm import ObjectNotFound
from ..db import User
from ..py23 import strtype
class UserManager:
@ -24,7 +23,7 @@ class UserManager:
def get(uid):
if isinstance(uid, uuid.UUID):
pass
elif isinstance(uid, strtype):
elif isinstance(uid, str):
uid = uuid.UUID(uid)
else:
raise ValueError("Invalid user id")

View File

@ -1,70 +0,0 @@
# coding: utf-8
#
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
# Copyright (C) 2018-2019 Alban 'spl0k' Féron
# 2018-2019 Carey 'pR0Ps' Metcalfe
#
# Distributed under terms of the GNU AGPLv3 license.
# Try built-in scandir, fall back to the package for Python 2.7
try:
from os import scandir
except ImportError:
from scandir import scandir
# os.replace was added in Python 3.3, provide a fallback for Python 2.7
try:
from os import replace as osreplace
except ImportError:
# os.rename is equivalent to os.replace except on Windows
# On Windows an existing file will not be overwritten
# This fallback just attempts to delete the dst file before using rename
import sys
if sys.platform != "win32":
from os import rename as osreplace
else:
import os
def osreplace(src, dst):
try:
os.remove(dst)
except OSError:
pass
os.rename(src, dst)
try:
from queue import Queue, Empty as QueueEmpty
except ImportError:
from Queue import Queue, Empty as QueueEmpty
try:
# Python 2
strtype = basestring
_builtin_dict = dict
class DictMeta(type):
def __instancecheck__(cls, instance):
return isinstance(instance, _builtin_dict)
class dict(dict):
__metaclass__ = DictMeta
def keys(self):
return self.viewkeys()
def values(self):
return self.viewvalues()
def items(self):
return self.viewitems()
except NameError:
# Python 3
strtype = str
dict = dict

View File

@ -14,13 +14,13 @@ import time
from datetime import datetime
from pony.orm import db_session
from queue import Queue, Empty as QueueEmpty
from threading import Thread, Event
from .covers import find_cover_in_folder, has_embedded_cover, CoverFile
from .db import Folder, Artist, Album, Track, User
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from .db import RatingFolder, RatingTrack
from .py23 import scandir, strtype, Queue, QueueEmpty
logger = logging.getLogger(__name__)
@ -93,7 +93,7 @@ class Scanner(Thread):
self.__progress(folder_name, scanned)
def queue_folder(self, folder_name):
if not isinstance(folder_name, strtype):
if not isinstance(folder_name, str):
raise TypeError("Expecting string, got " + str(type(folder_name)))
self.__queue.put(folder_name)
@ -131,7 +131,7 @@ class Scanner(Thread):
scanned = 0
while not self.__stopped.is_set() and to_scan:
path = to_scan.pop()
for entry in scandir(path):
for entry in os.scandir(path):
if entry.name.startswith("."):
continue
if entry.is_symlink() and not self.__follow_symlinks:
@ -194,7 +194,7 @@ class Scanner(Thread):
@db_session
def scan_file(self, path_or_direntry):
if isinstance(path_or_direntry, strtype):
if isinstance(path_or_direntry, str):
path = path_or_direntry
if not os.path.exists(path):
@ -286,7 +286,7 @@ class Scanner(Thread):
@db_session
def remove_file(self, path):
if not isinstance(path, strtype):
if not isinstance(path, str):
raise TypeError("Expecting string, got " + str(type(path)))
tr = Track.get(path=path)
@ -298,9 +298,9 @@ class Scanner(Thread):
@db_session
def move_file(self, src_path, dst_path):
if not isinstance(src_path, strtype):
if not isinstance(src_path, str):
raise TypeError("Expecting string, got " + str(type(src_path)))
if not isinstance(dst_path, strtype):
if not isinstance(dst_path, str):
raise TypeError("Expecting string, got " + str(type(dst_path)))
if src_path == dst_path:
@ -326,7 +326,7 @@ class Scanner(Thread):
@db_session
def find_cover(self, dirpath):
if not isinstance(dirpath, strtype): # pragma: nocover
if not isinstance(dirpath, str): # pragma: nocover
raise TypeError("Expecting string, got " + str(type(dirpath)))
if not os.path.exists(dirpath):
@ -346,7 +346,7 @@ class Scanner(Thread):
@db_session
def add_cover(self, path):
if not isinstance(path, strtype): # pragma: nocover
if not isinstance(path, str): # pragma: nocover
raise TypeError("Expecting string, got " + str(type(path)))
folder = Folder.get(path=os.path.dirname(path))

View File

@ -18,7 +18,6 @@ from watchdog.events import PatternMatchingEventHandler
from . import covers
from .db import Folder
from .py23 import dict, strtype
from .scanner import Scanner
OP_SCAN = 1
@ -267,7 +266,7 @@ class SupysonicWatcher(object):
def add_folder(self, folder):
if isinstance(folder, Folder):
path = folder.path
elif isinstance(folder, strtype):
elif isinstance(folder, str):
path = folder
else:
raise TypeError("Expecting string or Folder, got " + str(type(folder)))
@ -279,7 +278,7 @@ class SupysonicWatcher(object):
def remove_folder(self, folder):
if isinstance(folder, Folder):
path = folder.path
elif isinstance(folder, strtype):
elif isinstance(folder, str):
path = folder
else:
raise TypeError("Expecting string or Folder, got " + str(type(folder)))

View File

@ -11,7 +11,6 @@ import re
from lxml import etree
from supysonic.managers.user import UserManager
from supysonic.py23 import strtype
from ..testbase import TestBase
@ -68,7 +67,7 @@ class ApiTestBase(TestBase):
if not isinstance(args, dict):
raise TypeError("'args', expecting a dict, got " + type(args).__name__)
if tag and not isinstance(tag, strtype):
if tag and not isinstance(tag, str):
raise TypeError("'tag', expecting a str, got " + type(tag).__name__)
args.update({"c": "tests", "v": "1.9.0"})

View File

@ -14,7 +14,6 @@ import flask.json
from xml.etree import ElementTree
from supysonic.api.formatters import JSONFormatter, JSONPFormatter, XMLFormatter
from supysonic.py23 import strtype
from ..testbase import TestBase
@ -98,7 +97,7 @@ class ResponseHelperJsonTestCase(TestBase, UnwrapperMixin.create_from(JSONFormat
self.assertIn("list", d)
self.assertNotIn("emptyList", d)
self.assertIn("subdict", d)
self.assertIsInstance(d["value"], strtype)
self.assertIsInstance(d["value"], str)
self.assertIsInstance(d["list"], list)
self.assertIsInstance(d["subdict"], dict)

View File

@ -15,13 +15,9 @@ import tempfile
import unittest
from contextlib import contextmanager
from io import StringIO
from pony.orm import db_session
try: # Don't use io.StringIO on py2, it only accepts unicode and the CLI spits strs
from StringIO import StringIO
except ImportError:
from io import StringIO
from supysonic.db import Folder, User, init_database, release_database
from supysonic.cli import SupysonicCLI

View File

@ -11,7 +11,6 @@
import unittest
from supysonic.config import IniConfig
from supysonic.py23 import strtype
class ConfigTestCase(unittest.TestCase):
@ -26,7 +25,7 @@ class ConfigTestCase(unittest.TestCase):
self.assertIsInstance(conf.TYPES["float"], float)
self.assertIsInstance(conf.TYPES["int"], int)
self.assertIsInstance(conf.TYPES["string"], strtype)
self.assertIsInstance(conf.TYPES["string"], str)
for t in ("bool", "switch", "yn"):
self.assertIsInstance(conf.BOOLEANS[t + "_false"], bool)

View File

@ -11,7 +11,6 @@
from supysonic import db
from supysonic.managers.user import UserManager
from supysonic.py23 import strtype
import io
import unittest