diff --git a/.idea/misc.xml b/.idea/misc.xml index 02b0401..46766cb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ + + \ No newline at end of file diff --git a/app/config.py b/app/config.py index 8ba7fb5..b31bf4d 100644 --- a/app/config.py +++ b/app/config.py @@ -36,15 +36,8 @@ class Config(object): MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None MAX_INFO_TABS = 3 MILLISECOND_SIGFIGS = 0 - MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev" # noqa E501 + MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2" # noqa E501 NORMALISE_ON_IMPORT = True - NOTE_COLOURS = { - 'track': "#ffff00", - 'request': "#7cf000", - 'wrap': "#fffacd", - 'this month then': "#c256c2", - 'story': "#dda0dd", - } ROOT = os.environ.get('ROOT') or "/home/kae/music" TESTMODE = True TIMER_MS = 500 diff --git a/app/models.py b/app/models.py index 80fb963..e277472 100644 --- a/app/models.py +++ b/app/models.py @@ -19,7 +19,7 @@ from sqlalchemy import ( ) from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound -from sqlalchemy.orm import relationship, sessionmaker +from sqlalchemy.orm import relationship, sessionmaker, scoped_session from config import Config from log import DEBUG, ERROR @@ -27,16 +27,26 @@ from log import DEBUG, ERROR # Create session at the global level as per # https://docs.sqlalchemy.org/en/13/orm/session_basics.html -# Set up database connection -engine = sqlalchemy.create_engine(f"{Config.MYSQL_CONNECT}?charset=utf8", - encoding='utf-8', - echo=Config.DISPLAY_SQL, - pool_pre_ping=True) Base = declarative_base() -Base.metadata.create_all(engine) +Session = scoped_session(sessionmaker()) -# Create a Session factory -Session = sessionmaker(bind=engine) + +def dbinit(): + # Set up database connection + + global Session + + engine = sqlalchemy.create_engine( + f"{Config.MYSQL_CONNECT}?charset=utf8", + encoding='utf-8', + echo=Config.DISPLAY_SQL, + pool_pre_ping=True) + + Session.configure(bind=engine) + Base.metadata.create_all(engine) + + # Create a Session factory + Session = sessionmaker(bind=engine) # Database classes @@ -634,16 +644,16 @@ class Tracks(Base): def search_artists(session, text): return ( session.query(Tracks) - .filter(Tracks.artist.ilike(f"%{text}%")) - .order_by(Tracks.title) + .filter(Tracks.artist.ilike(f"%{text}%")) + .order_by(Tracks.title) ).all() @staticmethod def search_titles(session, text): return ( session.query(Tracks) - .filter(Tracks.title.ilike(f"%{text}%")) - .order_by(Tracks.title) + .filter(Tracks.title.ilike(f"%{text}%")) + .order_by(Tracks.title) ).all() @staticmethod diff --git a/app/musicmuster.py b/app/musicmuster.py index efb264c..4d7d765 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -27,8 +27,8 @@ import helpers import music from config import Config -from model import (Notes, Playdates, Playlists, PlaylistTracks, - Session, Settings, Tracks) +from models import (dbinit, Notes, Playdates, Playlists, PlaylistTracks, + Session, Settings, Tracks) from playlists import PlaylistTab from songdb import create_track_from_file from ui.dlg_search_database_ui import Ui_Dialog @@ -226,12 +226,14 @@ class Window(QMainWindow, Ui_MainWindow): def close_tab(self, index): if hasattr(self.tabPlaylist.widget(index), 'is_playlist'): - if self.tabPlaylist.widget(index) == self.current_track_playlist_tab: - self.statusbar.showMessage("Can't close current track playlist", - 5000) + if self.tabPlaylist.widget(index) == ( + self.current_track_playlist_tab): + self.statusbar.showMessage( + "Can't close current track playlist", 5000) return if self.tabPlaylist.widget(index) == self.next_track_playlist_tab: - self.statusbar.showMessage("Can't close next track playlist", 5000) + self.statusbar.showMessage( + "Can't close next track playlist", 5000) return # It's OK to close this playlist so remove from open playlist list with Session() as session: @@ -950,6 +952,7 @@ class SelectPlaylistDialog(QDialog): def main(): try: app = QApplication(sys.argv) + dbinit() win = Window() win.show() sys.exit(app.exec()) diff --git a/app/playlists.py b/app/playlists.py index 9942ac1..86e65d1 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -19,7 +19,7 @@ from config import Config from datetime import datetime, timedelta from helpers import get_relative_date, open_in_audacity from log import DEBUG, ERROR -from model import ( +from models import ( Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks, NoteColours ) from songdb import create_track_from_file, update_meta diff --git a/app/songdb.py b/app/songdb.py index c3ace97..17a189a 100755 --- a/app/songdb.py +++ b/app/songdb.py @@ -8,7 +8,7 @@ import tempfile from config import Config from helpers import show_warning from log import DEBUG, INFO -from model import Notes, Playdates, PlaylistTracks, Session, Tracks +from models import Notes, Playdates, PlaylistTracks, Session, Tracks from mutagen.flac import FLAC from mutagen.mp3 import MP3 from pydub import AudioSegment, effects diff --git a/conftest.py b/conftest.py index 97f95e7..533dddd 100644 --- a/conftest.py +++ b/conftest.py @@ -1,83 +1,56 @@ -# https://stewartadam.io/blog/2019/04/04/testing-flask-applications-code-database-views-flask-config-and-app-context-pytest +# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629 import pytest import sys +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker + sys.path.append("app") - -from config import Config # noqa E402 -from flask import g # noqa E402 -from kpi import create_app # noqa E402 -from kpi import db as _db # noqa E402 +from app.models import Base # noqa E402 -class TestConfig(Config): - # TESTING = True - SQLALCHEMY_DATABASE_URI = 'sqlite://' - PYTESTING = True +@pytest.fixture(scope="session") +def connection(): + engine = create_engine( + "mysql+mysqldb://musicmuster_testing:musicmuster_testing@" + "localhost/musicmuster_testing" + ) + return engine.connect() -# @pytest.fixture(scope="module") -# def app(): -# app = create_app(TestConfig) -# app_context = app.app_context() -# app_context.push() -# db.create_all() -# return app + +def seed_database(): + pass + + # users = [ + # { + # "id": 1, + # "name": "John Doe", + # }, + # # ... + # ] + + # for user in users: + # db_user = User(**user) + # db_session.add(db_user) + # db_session.commit() + + +@pytest.fixture(scope="session") +def setup_database(connection): + Base.metadata.bind = connection + Base.metadata.create_all() + # seed_database() + + yield + + models.Base.metadata.drop_all() @pytest.fixture -def no_bank_holidays(): - "Set no bank holidays" - - g.BankHolidays = [] - - -@pytest.fixture(scope="session") -def app(request): - """Test session-wide test `Flask` application.""" - - app = create_app(TestConfig) - return app - - -@pytest.fixture(autouse=True) -def _setup_app_context_for_test(request, app): - """ - Given app is session-wide, sets up a app context per test to ensure that - app and request stack is not shared between tests. - """ - - ctx = app.app_context() - ctx.push() - yield # tests will run here - ctx.pop() - - -@pytest.fixture(scope="session") -def db(app, request): - """Returns session-wide initialized database""" - - with app.app_context(): - _db.create_all() - yield _db - _db.drop_all() - - -@pytest.fixture(scope="function") -def session(app, db, request): - """Creates a new database session for each test, - rolling back changes afterwards""" - - connection = _db.engine.connect() +def db_session(setup_database, connection): transaction = connection.begin() - - options = dict(bind=connection, binds={}) - session = _db.create_scoped_session(options=options) - - _db.session = session - - yield session - + yield scoped_session( + sessionmaker(autocommit=False, autoflush=False, bind=connection) + ) transaction.rollback() - connection.close() - session.remove() diff --git a/conftest.py.2b b/conftest.py.2b deleted file mode 100644 index 97f95e7..0000000 --- a/conftest.py.2b +++ /dev/null @@ -1,83 +0,0 @@ -# https://stewartadam.io/blog/2019/04/04/testing-flask-applications-code-database-views-flask-config-and-app-context-pytest - -import pytest -import sys - -sys.path.append("app") - -from config import Config # noqa E402 -from flask import g # noqa E402 -from kpi import create_app # noqa E402 -from kpi import db as _db # noqa E402 - - -class TestConfig(Config): - # TESTING = True - SQLALCHEMY_DATABASE_URI = 'sqlite://' - PYTESTING = True - -# @pytest.fixture(scope="module") -# def app(): -# app = create_app(TestConfig) -# app_context = app.app_context() -# app_context.push() -# db.create_all() -# return app - - -@pytest.fixture -def no_bank_holidays(): - "Set no bank holidays" - - g.BankHolidays = [] - - -@pytest.fixture(scope="session") -def app(request): - """Test session-wide test `Flask` application.""" - - app = create_app(TestConfig) - return app - - -@pytest.fixture(autouse=True) -def _setup_app_context_for_test(request, app): - """ - Given app is session-wide, sets up a app context per test to ensure that - app and request stack is not shared between tests. - """ - - ctx = app.app_context() - ctx.push() - yield # tests will run here - ctx.pop() - - -@pytest.fixture(scope="session") -def db(app, request): - """Returns session-wide initialized database""" - - with app.app_context(): - _db.create_all() - yield _db - _db.drop_all() - - -@pytest.fixture(scope="function") -def session(app, db, request): - """Creates a new database session for each test, - rolling back changes afterwards""" - - connection = _db.engine.connect() - transaction = connection.begin() - - options = dict(bind=connection, binds={}) - session = _db.create_scoped_session(options=options) - - _db.session = session - - yield session - - transaction.rollback() - connection.close() - session.remove() diff --git a/migrations/env.py b/migrations/env.py index fb0ec86..4790b3c 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -24,7 +24,7 @@ fileConfig(config.config_file_name) path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, path) sys.path.insert(0, os.path.join(path, "app")) -from app.model import Base +from app.models import Base target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/poetry.lock b/poetry.lock index 3a11bbe..e32f9d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -353,6 +353,27 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pytest" +version = "7.0.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + [[package]] name = "python-vlc" version = "3.0.12118" @@ -804,6 +825,10 @@ pyqtwebengine-qt5 = [ {file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9e80b408d8de09d4e708d5d84c3ceaf3603292ff8f5e566ae44bb0320fa59c33"}, {file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:24231f19e1595018779977de6722b5c69f3d03f34a5f7574ff21cd1e764ef76d"}, ] +pytest = [ + {file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"}, + {file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"}, +] python-vlc = [ {file = "python-vlc-3.0.12118.tar.gz", hash = "sha256:566f2f7c303f6800851cacc016df1c6eeec094ad63e0a49d87db9d698094f1fb"}, {file = "python_vlc-3.0.12118-py3-none-any.whl", hash = "sha256:f88be06c6f819a4db2de1c586b193b5df1410ff10fca33b8c6f4e56037c46f7b"}, diff --git a/pyproject.toml b/pyproject.toml index ec8d5ff..1a0aa18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ PyQt5-sip = "^12.9.0" [tool.poetry.dev-dependencies] mypy = "^0.931" +pytest = "^7.0.0" ipdb = "^0.13.9" [build-system] diff --git a/tags.lock b/tags.lock deleted file mode 100644 index bdefaba..0000000 --- a/tags.lock +++ /dev/null @@ -1 +0,0 @@ -3149370 diff --git a/test_model.py b/test_model.py index b659ecd..855b0eb 100644 --- a/test_model.py +++ b/test_model.py @@ -1,7 +1,6 @@ -from model import Playlists, Session +from app.models import Playlists, Session -def test_get_colour(): - with Session() as session: - x = Playlists.get_all_playlists(session) +def test_get_colour(db_session): + x = Playlists.get_all_playlists(db_session) assert False