diff --git a/.gitignore b/.gitignore index 8802201..a60c4d3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,15 +22,14 @@ linux/flatpak/.flatpak-builder **/__pycache__/ .ropeproject .vscode +*.kdev4 +.kdev4 +.coverage build docs/html .directory -*.kdev4 *.lpf *.lgg *.spec -.kdev4 -AccountFree.pro -AccountFree.pro.user *.egg-info/ *.tar.gz diff --git a/LogarithmPlotter/__init__.py b/LogarithmPlotter/__init__.py index 9c675a2..b3140ad 100644 --- a/LogarithmPlotter/__init__.py +++ b/LogarithmPlotter/__init__.py @@ -20,13 +20,13 @@ from shutil import which __VERSION__ = "0.6.0" is_release = False - # Check if development version, if so get the date of the latest git patch # and append it to the version string. if not is_release and which('git') is not None: from os.path import realpath, join, dirname, exists from subprocess import check_output from datetime import datetime + # Command to check date of latest git commit cmd = ['git', 'log', '--format=%ci', '-n 1'] cwd = realpath(join(dirname(__file__), '..')) # Root AccountFree directory. @@ -41,4 +41,5 @@ if not is_release and which('git') is not None: if __name__ == "__main__": from .logarithmplotter import run + run() diff --git a/LogarithmPlotter/logarithmplotter.py b/LogarithmPlotter/logarithmplotter.py index 1af01d1..ae908f5 100644 --- a/LogarithmPlotter/logarithmplotter.py +++ b/LogarithmPlotter/logarithmplotter.py @@ -87,7 +87,6 @@ def run(): icon_fallbacks.append(path.realpath(path.join(base_icon_path, "settings", "custom"))) QIcon.setFallbackSearchPaths(icon_fallbacks); - QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) app = QApplication(argv) app.setApplicationName("LogarithmPlotter") app.setDesktopFileName("eu.ad5001.LogarithmPlotter.desktop") @@ -104,10 +103,10 @@ def run(): app.installTranslator(translator); # Installing macOS file handler. - macOSFileOpenHandler = None + macos_file_open_handler = None if platform == "darwin": - macOSFileOpenHandler = native.MacOSFileOpenHandler() - app.installEventFilter(macOSFileOpenHandler) + macos_file_open_handler = native.MacOSFileOpenHandler() + app.installEventFilter(macos_file_open_handler) engine = QQmlApplicationEngine() global tmpfile @@ -138,7 +137,7 @@ def run(): chdir(path.dirname(path.realpath(__file__))) if platform == "darwin": - macOSFileOpenHandler.init_io(js_globals.Modules.IO) + macos_file_open_handler.init_io(js_globals.Modules.IO) # Check for LaTeX installation if LaTeX support is enabled if config.getSetting("enable_latex"): diff --git a/LogarithmPlotter/util/config.py b/LogarithmPlotter/util/config.py index 5c2c653..d295caa 100644 --- a/LogarithmPlotter/util/config.py +++ b/LogarithmPlotter/util/config.py @@ -21,7 +21,6 @@ from platform import system from json import load, dumps from PySide6.QtCore import QLocale, QTranslator - DEFAULT_SETTINGS = { "check_for_updates": True, "reset_redo_stack": True, @@ -38,7 +37,7 @@ DEFAULT_SETTINGS = { "default_graph": { "xzoom": 100, "yzoom": 10, - "xmin": 5/10, + "xmin": 5 / 10, "ymax": 25, "xaxisstep": "4", "yaxisstep": "4", @@ -54,40 +53,48 @@ DEFAULT_SETTINGS = { # Create config directory CONFIG_PATH = { - "Linux": path.join(environ["XDG_CONFIG_HOME"], "LogarithmPlotter") if "XDG_CONFIG_HOME" in environ else path.join(path.expanduser("~"), ".config", "LogarithmPlotter"), + "Linux": path.join(environ["XDG_CONFIG_HOME"], "LogarithmPlotter") + if "XDG_CONFIG_HOME" in environ else + path.join(path.expanduser("~"), ".config", "LogarithmPlotter"), "Windows": path.join(path.expandvars('%APPDATA%'), "LogarithmPlotter", "config"), "Darwin": path.join(path.expanduser("~"), "Library", "Application Support", "LogarithmPlotter"), }[system()] CONFIG_FILE = path.join(CONFIG_PATH, "config.json") -initialized = False current_config = DEFAULT_SETTINGS +class UnknownNamespaceError(Exception): pass + + def init(): """ Initializes the config and loads all possible settings from the file if needs be. """ + global current_config + current_config = DEFAULT_SETTINGS makedirs(CONFIG_PATH, exist_ok=True) if path.exists(CONFIG_FILE): - cfg_data = load(open(CONFIG_FILE, 'r', -1, 'utf8')) + cfg_data = load(open(CONFIG_FILE, 'r', -1, 'utf8')) for setting_name in cfg_data: if type(cfg_data[setting_name]) == dict: for setting_name2 in cfg_data[setting_name]: - setSetting(setting_name+"."+setting_name2, cfg_data[setting_name][setting_name2]) + setSetting(setting_name + "." + setting_name2, cfg_data[setting_name][setting_name2]) else: setSetting(setting_name, cfg_data[setting_name]) - -def save(): + + +def save(file=CONFIG_FILE): """ Saves the config to the path. """ - write_file = open(CONFIG_FILE, 'w', -1, 'utf8') + write_file = open(file, 'w', -1, 'utf8') write_file.write(dumps(current_config)) write_file.close() + def getSetting(namespace): """ Returns a setting from a namespace. @@ -101,9 +108,10 @@ def getSetting(namespace): setting = setting[name] else: # return namespace # Return original name - raise ValueError('Setting ' + namespace + ' doesn\'t exist. Debug: ', setting, name) + raise UnknownNamespaceError(f"Setting {namespace} doesn't exist. Debug: {setting}, {name}") return setting + def setSetting(namespace, data): """ Sets a setting at a namespace with data. @@ -117,6 +125,6 @@ def setSetting(namespace, data): if name in setting: setting = setting[name] else: - raise ValueError('Setting {} doesn\'t exist. Debug: {}, {}'.format(namespace, setting, name)) + raise UnknownNamespaceError(f"Setting {namespace} doesn't exist. Debug: {setting}, {name}") else: setting[name] = data diff --git a/LogarithmPlotter/util/helper.py b/LogarithmPlotter/util/helper.py index 47e7395..c7c92d8 100644 --- a/LogarithmPlotter/util/helper.py +++ b/LogarithmPlotter/util/helper.py @@ -32,6 +32,9 @@ from LogarithmPlotter import __VERSION__ from LogarithmPlotter.util import config +class InvalidFileException(Exception): pass + + class ChangelogFetcher(QRunnable): def __init__(self, helper): QRunnable.__init__(self) @@ -94,16 +97,16 @@ class Helper(QObject): pass elif data[:3] == "LPF": # More recent version of LogarithmPlotter file, but incompatible with the current format - raise Exception(QCoreApplication.translate("This file was created by a more recent version of LogarithmPlotter and cannot be backloaded in LogarithmPlotter v{}.\nPlease update LogarithmPlotter to open this file.".format(__VERSION__))) + msg = QCoreApplication.translate("This file was created by a more recent version of LogarithmPlotter and cannot be backloaded in LogarithmPlotter v{}.\nPlease update LogarithmPlotter to open this file.") + raise InvalidFileException(msg.format(__VERSION__)) else: raise Exception("Invalid LogarithmPlotter file.") except Exception as e: # If file can't be loaded - QMessageBox.warning(None, 'LogarithmPlotter', - QCoreApplication.translate('main', 'Could not open file "{}":\n{}').format(filename, - e), - QMessageBox.Ok) # Cannot parse file + msg = QCoreApplication.translate('main', 'Could not open file "{}":\n{}') + QMessageBox.warning(None, 'LogarithmPlotter', msg.format(filename, e), QMessageBox.Ok) # Cannot parse file else: - QMessageBox.warning(None, 'LogarithmPlotter', QCoreApplication.translate('main', 'Could not open file: "{}"\nFile does not exist.').format(filename), QMessageBox.Ok) # Cannot parse file + msg = QCoreApplication.translate('main', 'Could not open file: "{}"\nFile does not exist.') + QMessageBox.warning(None, 'LogarithmPlotter', msg.format(filename), QMessageBox.Ok) # Cannot parse file try: chdir(path.dirname(path.realpath(__file__))) except NotADirectoryError as e: @@ -131,7 +134,6 @@ class Helper(QObject): @Slot(str, result=float) def getSettingInt(self, namespace): - print('Getting', namespace, config.getSetting(namespace)) return config.getSetting(namespace) @Slot(str, result=bool) @@ -159,9 +161,8 @@ class Helper(QObject): """ Returns the version info about Qt, PySide6 & Python """ - return QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}").format(PySide6_version, - sys_version.split( - "\n")[0]) + msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}") + return msg.format(PySide6_version, sys_version.split("\n")[0]) @Slot() def fetchChangelog(self): diff --git a/LogarithmPlotter/util/js.py b/LogarithmPlotter/util/js.py index 4514a42..b00c645 100644 --- a/LogarithmPlotter/util/js.py +++ b/LogarithmPlotter/util/js.py @@ -18,11 +18,13 @@ from PySide6.QtQml import QJSValue +class InvalidAttributeValueException(Exception): pass class PyJSValue: """ Wrapper to provide easy way to interact with JavaScript values in Python directly. """ + def __init__(self, js_value: QJSValue, parent: QJSValue = None): self.qjs_value = js_value self._parent = parent @@ -37,16 +39,32 @@ class PyJSValue: elif isinstance(value, PyJSValue): # Set property self.qjs_value.setProperty(key, value.qjs_value) - else: - print('Setting', key, value) + elif isinstance(value, QJSValue): self.qjs_value.setProperty(key, value) + elif type(value) in (int, float, str, bool): + self.qjs_value.setProperty(key, QJSValue(value)) + else: + raise InvalidAttributeValueException(f"Invalid value {value} of type {type(value)} being set to {key}.") + + def __eq__(self, other): + if isinstance(other, PyJSValue): + return self.qjs_value.equals(other.qjs_value) + elif isinstance(other, QJSValue): + return self.qjs_value.equals(other) + elif type(other) in (int, float, str, bool): + return self.qjs_value.equals(QJSValue(other)) + else: + return False def __call__(self, *args, **kwargs): + value = None if self.qjs_value.isCallable(): if self._parent is None: - return self.qjs_value.call(args) + value = self.qjs_value.call(args) else: - return self.qjs_value.callWithInstance(self._parent, args) + value = self.qjs_value.callWithInstance(self._parent, args) else: - raise ValueError('Cannot call non-function JS value.') - + raise InvalidAttributeValueException('Cannot call non-function JS value.') + if isinstance(value, QJSValue): + value = PyJSValue(value) + return value diff --git a/LogarithmPlotter/util/latex.py b/LogarithmPlotter/util/latex.py index 166e224..6b31359 100644 --- a/LogarithmPlotter/util/latex.py +++ b/LogarithmPlotter/util/latex.py @@ -66,11 +66,11 @@ class Latex(QObject): self.tempdir = tempdir @Property(bool) - def latexSupported(self): + def latexSupported(self) -> bool: return LATEX_PATH is not None and DVIPNG_PATH is not None @Slot(result=bool) - def checkLatexInstallation(self): + def checkLatexInstallation(self) -> bool: """ Checks if the current latex installation is valid. """ @@ -78,19 +78,22 @@ class Latex(QObject): if LATEX_PATH is None: print("No Latex installation found.") if "--test-build" not in argv: - QMessageBox.warning(None, "LogarithmPlotter - Latex setup", - QCoreApplication.translate("latex", "No Latex installation found.\nIf you already have a latex distribution installed, make sure it's installed on your path.\nOtherwise, you can download a Latex distribution like TeX Live at https://tug.org/texlive/.")) + msg = QCoreApplication.translate("latex", + "No Latex installation found.\nIf you already have a latex distribution installed, make sure it's installed on your path.\nOtherwise, you can download a Latex distribution like TeX Live at https://tug.org/texlive/.") + QMessageBox.warning(None, "LogarithmPlotter - Latex setup", msg) valid_install = False elif DVIPNG_PATH is None: print("DVIPNG not found.") if "--test-build" not in argv: - QMessageBox.warning(None, "LogarithmPlotter - Latex setup", QCoreApplication.translate("latex", "DVIPNG was not found. Make sure you include it from your Latex distribution.")) + msg = QCoreApplication.translate("latex", + "DVIPNG was not found. Make sure you include it from your Latex distribution.") + QMessageBox.warning(None, "LogarithmPlotter - Latex setup", msg) valid_install = False else: try: self.render("", 14, QColor(0, 0, 0, 255)) except Exception as e: - valid_install = False # Should have sent an error message if failed to render + valid_install = False # Should have sent an error message if failed to render return valid_install @Slot(str, float, QColor, result=str) @@ -119,9 +122,9 @@ class Latex(QObject): img = QImage(export_path) # Small hack, not very optimized since we load the image twice, but you can't pass a QImage to QML and expect it to be loaded return f'{export_path}.png,{img.width()},{img.height()}' - + @Slot(str, float, QColor, result=str) - def findPrerendered(self, latex_markup: str, font_size: float, color: QColor): + def findPrerendered(self, latex_markup: str, font_size: float, color: QColor) -> str: """ Finds a prerendered image and returns its data if possible, and an empty string if not. """ @@ -131,8 +134,7 @@ class Latex(QObject): img = QImage(export_path) data = f'{export_path}.png,{img.width()},{img.height()}' return data - - + def create_export_path(self, latex_markup: str, font_size: float, color: QColor): """ Standardizes export path for renders. @@ -140,7 +142,6 @@ class Latex(QObject): markup_hash = "render" + str(hash(latex_markup)) export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}') return markup_hash, export_path - def create_latex_doc(self, export_path: str, latex_markup: str): """ @@ -183,16 +184,18 @@ class Latex(QObject): """ Runs a subprocess and handles exceptions and messages them to the user. """ + cmd = " ".join(process) proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir.name) try: out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long. if proc.returncode != 0: # Process errored output = str(out, 'utf8') + "\n" + str(err, 'utf8') - QMessageBox.warning(None, "LogarithmPlotter - Latex", - QCoreApplication.translate("latex", "An exception occured within the creation of the latex formula.\nProcess '{}' ended with a non-zero return code {}:\n\n{}\nPlease make sure your latex installation is correct and report a bug if so.") - .format(" ".join(process), proc.returncode, output)) - raise Exception("{0} process exited with return code {1}:\n{2}\n{3}".format(" ".join(process), str(proc.returncode), str(out, 'utf8'), str(err, 'utf8'))) + msg = QCoreApplication.translate("latex", + "An exception occured within the creation of the latex formula.\nProcess '{}' ended with a non-zero return code {}:\n\n{}\nPlease make sure your latex installation is correct and report a bug if so.") + msg = msg.format(cmd, proc.returncode, output) + QMessageBox.warning(None, "LogarithmPlotter - Latex", msg) + raise Exception(f"{cmd} process exited with return code {str(proc.returncode)}:\n{str(out, 'utf8')}\n{str(err, 'utf8')}") except TimeoutExpired as e: # Process timed out proc.kill() @@ -202,14 +205,14 @@ class Latex(QObject): for pkg in PACKAGES: if f'{pkg}.sty' in output: # Package missing. - QMessageBox.warning(None, "LogarithmPlotter - Latex", - QCoreApplication.translate("latex", "Your LaTeX installation does not include some required packages:\n\n- {} (https://ctan.org/pkg/{})\n\nMake sure said package is installed, or disable the LaTeX rendering in LogarithmPlotter.") - .format(pkg, pkg)) + msg = QCoreApplication.translate("latex", + "Your LaTeX installation does not include some required packages:\n\n- {} (https://ctan.org/pkg/{})\n\nMake sure said package is installed, or disable the LaTeX rendering in LogarithmPlotter.") + QMessageBox.warning(None, "LogarithmPlotter - Latex", msg.format(pkg, pkg)) raise Exception("Latex: Missing package " + pkg) - QMessageBox.warning(None, "LogarithmPlotter - Latex", - QCoreApplication.translate("latex", "An exception occured within the creation of the latex formula.\nProcess '{}' took too long to finish:\n{}\nPlease make sure your latex installation is correct and report a bug if so.") - .format(" ".join(process), output)) - raise Exception(" ".join(process) + " process timed out:\n" + output) + msg = QCoreApplication.translate("latex", + "An exception occured within the creation of the latex formula.\nProcess '{}' took too long to finish:\n{}\nPlease make sure your latex installation is correct and report a bug if so.") + QMessageBox.warning(None, "LogarithmPlotter - Latex", msg.format(cmd, output)) + raise Exception(f"{cmd} process timed out:\n{output}") def cleanup(self, export_path): """ diff --git a/LogarithmPlotter/util/update.py b/LogarithmPlotter/util/update.py index dbacbfe..ed2b5ba 100644 --- a/LogarithmPlotter/util/update.py +++ b/LogarithmPlotter/util/update.py @@ -53,9 +53,8 @@ class UpdateCheckerRunnable(QRunnable): current_version_tuple = self.current_version.split(".") is_version_newer = version_tuple > current_version_tuple if is_version_newer: - msg_text = QCoreApplication.translate("update", - "An update for LogarithPlotter (v{}) is available.").format( - version) + msg_text = QCoreApplication.translate("update", "An update for LogarithmPlotter (v{}) is available.") + msg_text = msg_text.format(version) update_available = True else: show_alert = False @@ -63,11 +62,11 @@ class UpdateCheckerRunnable(QRunnable): except HTTPError as e: msg_text = QCoreApplication.translate("update", - "Could not fetch update information: Server error {}.").format( - str(e.code)) + "Could not fetch update information: Server error {}.") + msg_text = msg_text.format(str(e.code)) except URLError as e: - msg_text = QCoreApplication.translate("update", "Could not fetch update information: {}.").format( - str(e.reason)) + msg_text = QCoreApplication.translate("update", "Could not fetch update information: {}.") + msg_text = msg_text.format(str(e.reason)) self.callback.got_update_info.emit(show_alert, msg_text, update_available) diff --git a/README.md b/README.md index 7f092e3..0745682 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,6 @@ For all builds, you need [Python 3](https://python.org) with [PySide6](https://p - To build the snap, you need [snapcraft](https://snapcraft.io) installed. - Run `package-linux.sh`. - -### Linux - -Run `bash linux/install_local.sh` - ## Contribute There are several ways to contribute to LogarithmPlotter. @@ -54,6 +49,14 @@ There are several ways to contribute to LogarithmPlotter. - You can help the development of LogarithmPlotter. In order to get started, take a look at the [wiki](https://git.ad5001.eu/Ad5001/LogarithmPlotter/wiki/_pages). +## Tests + +To run LogarithmPlotter's test, use the following: + +- Python + - Install `pytest` and `pytest-cov` + - Run `pytest --cov` + ## Legal notice LogarithmPlotter - 2D plotter software to make BODE plots, sequences and repartition functions. Copyright (C) 2021-2024 Ad5001 diff --git a/tests/python/test_config.py b/tests/python/test_config.py new file mode 100644 index 0000000..493b0f1 --- /dev/null +++ b/tests/python/test_config.py @@ -0,0 +1,10 @@ +import unittest + + +class MyTestCase(unittest.TestCase): + def test_something(self): + self.assertEqual(True, False) # add assertion here + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/python/test_helper.py b/tests/python/test_helper.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/python/test_pyjs.py b/tests/python/test_pyjs.py new file mode 100644 index 0000000..e69de29