Commit 7749fc21 authored by Flavio TARSETTI's avatar Flavio TARSETTI
Browse files

Merge branch '225_check_prefix_on_startup' into 'v2'

Check prefix on startup

See merge request !98
parents 2db741c9 f71556e6
Pipeline #31608 passed with stage
in 10 minutes and 17 seconds
#!/usr/bin/env python
# -*- coding: utf-8 -*-
###############################################################################
# #
# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.editor module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform 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. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
"""
Decorators
"""
from functools import wraps
def frozen(cls):
"""
Don't allow new attributes to be added outside of init
Based on https://stackoverflow.com/a/29368642/5843716
"""
cls.__frozen = False
def frozensetattr(self, key, value):
"""Don't allow attributes to be added outside of __init__"""
if self.__frozen and not hasattr(self, key):
raise RuntimeError(
"Class {} is frozen. Cannot set {} = {}".format(
cls.__name__, key, value
)
)
else:
object.__setattr__(self, key, value)
def init_decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
func(self, *args, **kwargs)
self.__frozen = True
return wrapper
cls.__setattr__ = frozensetattr
cls.__init__ = init_decorator(cls.__init__)
return cls
......@@ -46,6 +46,8 @@ from beat.cmdline.decorators import raise_on_error
from beat.cmdline.decorators import verbosity_option
from ..utils import setup_logger
from ..utils import check_prefix_folders
from ..utils import check_prefix_dataformats
from ..widgets.mainwindow import MainWindow
from ..widgets.assetwidget import AssetWidget
from ..backend.asset import AssetType
......@@ -73,6 +75,14 @@ def setup_environment_cache(ctx, param, value):
dump_environments(environments)
def check_prefix(prefix_path):
"""Check that the prefix is usable"""
folder_status, _ = check_prefix_folders(prefix_path)
dataformat_status, _ = check_prefix_dataformats(prefix_path)
return folder_status and dataformat_status
refresh_environment_cache_flag = click.option(
"--no-check-env",
is_flag=True,
......@@ -130,6 +140,11 @@ def start(ctx):
"""Start the beat editor"""
app = QApplication(sys.argv)
config = ctx.meta["config"]
if not check_prefix(config.prefix):
return
app.installEventFilter(MouseWheelFilter(app))
mainwindow = MainWindow()
mainwindow.set_context(ctx)
......
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.editor module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform 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. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
import pytest
from ..decorators import frozen
def test_frozen():
@frozen
class FrozenClass:
pass
frozen_cls_instance = FrozenClass()
with pytest.raises(RuntimeError):
frozen_cls_instance.value = None
......@@ -30,37 +30,87 @@ Test the utils.py file
"""
import os
import tempfile
import pkg_resources
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMessageBox
from ..backend.asset import AssetType
from .. import utils
DATA_PATH = pkg_resources.resource_filename("beat.editor.test", "reference_data")
def test_Qt_version_equal():
assert utils.is_Qt_equal_or_higher(QtCore.QT_VERSION_STR)
def test_Qt_version_higher():
assert utils.is_Qt_equal_or_higher("4.8.7")
def test_Qt_version_smaller():
assert not utils.is_Qt_equal_or_higher("12.0.0")
def test_check_prefix_folders_yes(monkeypatch):
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
with tempfile.TemporaryDirectory() as prefix_folder:
result, modified = utils.check_prefix_folders(prefix_folder)
assert result
assert modified
for asset_type in AssetType:
if asset_type == AssetType.UNKNOWN:
continue
assert os.path.exists(os.path.join(prefix_folder, asset_type.path))
result, modified = utils.check_prefix_folders(prefix_folder)
assert result
assert not modified
def test_check_prefix_folders_no(monkeypatch):
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.No)
with tempfile.TemporaryDirectory() as prefix_folder:
result, modified = utils.check_prefix_folders(prefix_folder)
assert not result
assert not modified
assert os.listdir(prefix_folder) == []
def test_check_prefix_folders_with_prefix(monkeypatch, test_prefix):
result, modified = utils.check_prefix_folders(test_prefix)
assert result
assert not modified
def compare_with_reference(generated, reference_file_name):
""" Compare the given generated code with the content of the reference file
"""
with open(os.path.join(DATA_PATH, reference_file_name)) as reference_file:
reference = reference_file.read()
return generated == reference
def test_check_prefix_dataformats_yes(monkeypatch):
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
with tempfile.TemporaryDirectory() as prefix_folder:
result, modified = utils.check_prefix_dataformats(prefix_folder)
assert result
assert modified
asset_type = AssetType.DATAFORMAT
assert os.path.exists(
os.path.join(prefix_folder, asset_type.path, "user", "integers")
)
def test_generate_empty_database():
database = utils.generate_database()
assert compare_with_reference(database, "empty_database.py")
result, modified = utils.check_prefix_dataformats(prefix_folder)
assert result
assert not modified
def test_generate_empty_algorithm():
alg = {
"name": "user/alg/1",
"contents": {"splittable": True, "groups": [], "uses": {}},
}
algorithm = utils.generate_algorithm(alg["contents"])
assert compare_with_reference(algorithm, "empty_algorithm.py")
def test_check_prefix_dataformats_no(monkeypatch):
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.No)
with tempfile.TemporaryDirectory() as prefix_folder:
result, modified = utils.check_prefix_dataformats(prefix_folder)
assert not result
assert not modified
assert os.listdir(prefix_folder) == []
def test_generate_empty_library():
library = utils.generate_library()
assert compare_with_reference(library, "empty_library.py")
def test_check_prefix_dataformats_with_prefix(monkeypatch, test_prefix):
result, modified = utils.check_prefix_dataformats(test_prefix)
assert result
assert not modified
......@@ -34,147 +34,19 @@ import sys
import logging
import simplejson as json
import pkg_resources
import shutil
import jinja2
from functools import wraps
from packaging import version
from PyQt5 import QtCore
logger = logging.getLogger(__name__)
# Jinja2 environment for loading our templates
ENV = jinja2.Environment(
loader=jinja2.PackageLoader(__name__, "templates"),
autoescape=True,
keep_trailing_newline=True,
)
def generate_database(views=None):
"""Generates a valid BEAT database from our stored template
Parameters:
views (:py:class:`list`, Optional): A list of strings that represents the
views for the database
Returns:
str: The rendered template as a string
"""
views = views or ["View"]
template = ENV.get_template("database.jinja2")
return template.render(views=views)
def generate_library(uses=None):
"""Generates a valid BEAT library from our stored template
Parameters:
uses (:py:class:`dict`, Optional): A dict of other libraries that the
library uses. Keys are the value to reference the library, values are
the library being referenced.
Returns:
str: The rendered template as a string
"""
uses = uses or {}
template = ENV.get_template("library.jinja2")
return template.render(uses=uses)
def generate_algorithm(contents):
"""Generates a valid BEAT algorithm from our stored template
Parameters:
contents (:py:class:`dict`): The algorithm's JSON metadata
Returns:
str: The rendered template as a string
"""
template = ENV.get_template("algorithm.jinja2")
return template.render(contents=contents)
def generate_plotter(uses):
"""Generates a valid BEAT plotter from our stored template
Parameters:
contents (:py:class:`dict`): The plotter's JSON metadata
Returns:
str: The rendered template as a string
"""
uses = uses or {}
template = ENV.get_template("plotter.jinja2")
return template.render(uses=uses)
TEMPLATE_FUNCTION = dict(
databases=generate_database,
libraries=generate_library,
algorithms=generate_algorithm,
plotters=generate_plotter,
)
class PythonFileAlreadyExistsError(Exception):
pass
# Functions for template instantiation within beat.editor
def generate_python_template(entity, name, confirm, config, **kwargs):
"""Generates a template for a BEAT entity with the given named arguments
Parameters:
entity (str): A valid BEAT entity
name (str): The name of the object to have a python file generated for
confirm (:py:class:`boolean`): Whether to override the Python file if
one is found at the desired location
"""
resource_path = os.path.join(config.path, entity)
file_path = os.path.join(resource_path, name) + ".py"
if not confirm and os.path.isfile(file_path):
# python file already exists
raise PythonFileAlreadyExistsError
from PyQt5 import QtCore
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import QMessageBox
s = TEMPLATE_FUNCTION[entity](**kwargs)
from beat.core.algorithm import load_algorithm_prototype
with open(file_path, "w") as f:
f.write(s)
from .backend.asset import AssetType
return s
logger = logging.getLogger(__name__)
def setup_logger(name, verbosity):
......@@ -224,40 +96,6 @@ def setup_logger(name, verbosity):
return logger
def frozen(cls):
"""
Don't allow new attributes to be added outside of init
Based on https://stackoverflow.com/a/29368642/5843716
"""
cls.__frozen = False
def frozensetattr(self, key, value):
"""Don't allow attributes to be added outside of __init__"""
if self.__frozen and not hasattr(self, key):
print(
"Class {} is frozen. Cannot set {} = {}".format(
cls.__name__, key, value
)
)
else:
object.__setattr__(self, key, value)
def init_decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
func(self, *args, **kwargs)
self.__frozen = True
return wrapper
cls.__setattr__ = frozensetattr
cls.__init__ = init_decorator(cls.__init__)
return cls
def dataformat_basetypes():
"""Returns the list of base types that can be used for dataformat"""
......@@ -284,3 +122,76 @@ def dataformat_basetypes():
def is_Qt_equal_or_higher(version_string):
return version.parse(QtCore.QT_VERSION_STR) >= version.parse(version_string)
def check_prefix_folders(prefix_path):
"""Check that all supported asset types have their containing folder
available
"""
modified = False
result = True
missing_folders = []
for asset_type in AssetType:
if asset_type is not AssetType.UNKNOWN:
path = os.path.join(prefix_path, asset_type.path)
if not os.path.exists(path):
missing_folders.append(path)
if missing_folders:
answer = QMessageBox.question(
None,
QCoreApplication.translate("utils", "Prefix incomplete"),
QCoreApplication.translate(
"utils",
"Your prefix is missing folders.\n" "Would you like to create them ?",
),
)
if answer == QMessageBox.Yes:
for folder in missing_folders:
os.makedirs(folder)
modified = True
else:
result = False
return result, modified
def check_prefix_dataformats(prefix_path):
"""Currently checks that the data format needed for the algorithm is
available
"""
modified = False
result = True
try:
load_algorithm_prototype(prefix_path)
except RuntimeError:
answer = QMessageBox.question(
None,
QCoreApplication.translate("utils", "Prefix incomplete"),
QCoreApplication.translate(
"utils",
"Your prefix is missing a mandatory data format.\n"
"Would you like to create it ?",
),
)
if answer == QMessageBox.Yes:
asset_type = AssetType.DATAFORMAT
integers_path = os.path.join("prefix", asset_type.path, "user", "integers")
integers_folder = pkg_resources.resource_filename(
"beat.core.test", integers_path
)
shutil.copytree(
integers_folder,
os.path.join(prefix_path, asset_type.path, "user", "integers"),
)
modified = True
else:
result = False
return result, modified
......@@ -24,7 +24,7 @@
###############################################################################
from ..backend.asset import AssetType
from ..utils import frozen
from ..decorators import frozen
from .editor import AbstractAssetEditor
......
......@@ -44,7 +44,7 @@ from PyQt5.QtWidgets import QMessageBox
from ..backend.asset import AssetType
from ..backend.asset import Asset
from ..utils import frozen
from ..decorators import frozen
from .editor import PlaceholderEditor
from .algorithmeditor import AlgorithmEditor
......
......@@ -54,7 +54,7 @@ from beat.core.protocoltemplate import ProtocolTemplate
from ..backend.assetmodel import AssetModel
from ..backend.asset import AssetType
from ..utils import frozen
from ..decorators import frozen
from ..utils import is_Qt_equal_or_higher
from .editor import AbstractAssetEditor
......
......@@ -40,7 +40,7 @@ from PyQt5.QtWidgets import QSpinBox
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
from ..utils import frozen
from ..decorators import frozen
from ..utils import dataformat_basetypes
from ..backend.asset import AssetType
......
......@@ -24,7 +24,7 @@
###############################################################################
from ..backend.asset import AssetType
from ..utils import frozen
from ..decorators import frozen
from .editor import AbstractAssetEditor
......
......@@ -23,7 +23,7 @@
# #
###############################################################################
from ..utils import frozen
from ..decorators import frozen
from ..backend.asset import AssetType
from ..backend.assetmodel import AssetModel
......
......@@ -38,7 +38,7 @@ from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QWidget
from ..utils import frozen
from ..decorators import frozen
from ..backend.asset import AssetType
from ..backend.assetmodel import AssetModel
......
......@@ -24,7 +24,7 @@
###############################################################################
from ..backend.asset import AssetType
from ..utils import frozen
from ..decorators import frozen
from .editor import AbstractAssetEditor
......
......@@ -40,7 +40,7 @@ from PyQt5.QtWidgets import QTableWidgetItem
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
from ..utils import frozen
from ..decorators import frozen
from ..backend.asset import AssetType
from ..backend.assetmodel import AssetModel
......
......@@ -34,7 +34,7 @@ from PyQt5.QtGui import QValidator
from PyQt5.QtWidgets import QAbstractSpinBox