Compare commits

...

3 commits

Author SHA1 Message Date
9072e94d14
Starting python tests
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-18 00:30:58 +02:00
b50d56d511
Improving python coding format 2024-09-17 22:43:24 +02:00
1bf175b09c
Adding type hints 2024-09-17 22:26:17 +02:00
12 changed files with 112 additions and 71 deletions

7
.gitignore vendored
View file

@ -22,15 +22,14 @@ linux/flatpak/.flatpak-builder
**/__pycache__/ **/__pycache__/
.ropeproject .ropeproject
.vscode .vscode
*.kdev4
.kdev4
.coverage
build build
docs/html docs/html
.directory .directory
*.kdev4
*.lpf *.lpf
*.lgg *.lgg
*.spec *.spec
.kdev4
AccountFree.pro
AccountFree.pro.user
*.egg-info/ *.egg-info/
*.tar.gz *.tar.gz

View file

@ -20,13 +20,13 @@ from shutil import which
__VERSION__ = "0.6.0" __VERSION__ = "0.6.0"
is_release = False is_release = False
# Check if development version, if so get the date of the latest git patch # Check if development version, if so get the date of the latest git patch
# and append it to the version string. # and append it to the version string.
if not is_release and which('git') is not None: if not is_release and which('git') is not None:
from os.path import realpath, join, dirname, exists from os.path import realpath, join, dirname, exists
from subprocess import check_output from subprocess import check_output
from datetime import datetime from datetime import datetime
# Command to check date of latest git commit # Command to check date of latest git commit
cmd = ['git', 'log', '--format=%ci', '-n 1'] cmd = ['git', 'log', '--format=%ci', '-n 1']
cwd = realpath(join(dirname(__file__), '..')) # Root AccountFree directory. 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__": if __name__ == "__main__":
from .logarithmplotter import run from .logarithmplotter import run
run() run()

View file

@ -87,7 +87,6 @@ def run():
icon_fallbacks.append(path.realpath(path.join(base_icon_path, "settings", "custom"))) icon_fallbacks.append(path.realpath(path.join(base_icon_path, "settings", "custom")))
QIcon.setFallbackSearchPaths(icon_fallbacks); QIcon.setFallbackSearchPaths(icon_fallbacks);
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication(argv) app = QApplication(argv)
app.setApplicationName("LogarithmPlotter") app.setApplicationName("LogarithmPlotter")
app.setDesktopFileName("eu.ad5001.LogarithmPlotter.desktop") app.setDesktopFileName("eu.ad5001.LogarithmPlotter.desktop")
@ -104,10 +103,10 @@ def run():
app.installTranslator(translator); app.installTranslator(translator);
# Installing macOS file handler. # Installing macOS file handler.
macOSFileOpenHandler = None macos_file_open_handler = None
if platform == "darwin": if platform == "darwin":
macOSFileOpenHandler = native.MacOSFileOpenHandler() macos_file_open_handler = native.MacOSFileOpenHandler()
app.installEventFilter(macOSFileOpenHandler) app.installEventFilter(macos_file_open_handler)
engine = QQmlApplicationEngine() engine = QQmlApplicationEngine()
global tmpfile global tmpfile
@ -138,7 +137,7 @@ def run():
chdir(path.dirname(path.realpath(__file__))) chdir(path.dirname(path.realpath(__file__)))
if platform == "darwin": 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 # Check for LaTeX installation if LaTeX support is enabled
if config.getSetting("enable_latex"): if config.getSetting("enable_latex"):

View file

@ -21,7 +21,6 @@ from platform import system
from json import load, dumps from json import load, dumps
from PySide6.QtCore import QLocale, QTranslator from PySide6.QtCore import QLocale, QTranslator
DEFAULT_SETTINGS = { DEFAULT_SETTINGS = {
"check_for_updates": True, "check_for_updates": True,
"reset_redo_stack": True, "reset_redo_stack": True,
@ -38,7 +37,7 @@ DEFAULT_SETTINGS = {
"default_graph": { "default_graph": {
"xzoom": 100, "xzoom": 100,
"yzoom": 10, "yzoom": 10,
"xmin": 5/10, "xmin": 5 / 10,
"ymax": 25, "ymax": 25,
"xaxisstep": "4", "xaxisstep": "4",
"yaxisstep": "4", "yaxisstep": "4",
@ -54,40 +53,48 @@ DEFAULT_SETTINGS = {
# Create config directory # Create config directory
CONFIG_PATH = { 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"), "Windows": path.join(path.expandvars('%APPDATA%'), "LogarithmPlotter", "config"),
"Darwin": path.join(path.expanduser("~"), "Library", "Application Support", "LogarithmPlotter"), "Darwin": path.join(path.expanduser("~"), "Library", "Application Support", "LogarithmPlotter"),
}[system()] }[system()]
CONFIG_FILE = path.join(CONFIG_PATH, "config.json") CONFIG_FILE = path.join(CONFIG_PATH, "config.json")
initialized = False
current_config = DEFAULT_SETTINGS current_config = DEFAULT_SETTINGS
class UnknownNamespaceError(Exception): pass
def init(): def init():
""" """
Initializes the config and loads all possible settings from the file if needs be. 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) makedirs(CONFIG_PATH, exist_ok=True)
if path.exists(CONFIG_FILE): 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: for setting_name in cfg_data:
if type(cfg_data[setting_name]) == dict: if type(cfg_data[setting_name]) == dict:
for setting_name2 in cfg_data[setting_name]: 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: else:
setSetting(setting_name, cfg_data[setting_name]) setSetting(setting_name, cfg_data[setting_name])
def save():
def save(file=CONFIG_FILE):
""" """
Saves the config to the path. 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.write(dumps(current_config))
write_file.close() write_file.close()
def getSetting(namespace): def getSetting(namespace):
""" """
Returns a setting from a namespace. Returns a setting from a namespace.
@ -101,9 +108,10 @@ def getSetting(namespace):
setting = setting[name] setting = setting[name]
else: else:
# return namespace # Return original name # 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 return setting
def setSetting(namespace, data): def setSetting(namespace, data):
""" """
Sets a setting at a namespace with data. Sets a setting at a namespace with data.
@ -117,6 +125,6 @@ def setSetting(namespace, data):
if name in setting: if name in setting:
setting = setting[name] setting = setting[name]
else: else:
raise ValueError('Setting {} doesn\'t exist. Debug: {}, {}'.format(namespace, setting, name)) raise UnknownNamespaceError(f"Setting {namespace} doesn't exist. Debug: {setting}, {name}")
else: else:
setting[name] = data setting[name] = data

View file

@ -32,6 +32,9 @@ from LogarithmPlotter import __VERSION__
from LogarithmPlotter.util import config from LogarithmPlotter.util import config
class InvalidFileException(Exception): pass
class ChangelogFetcher(QRunnable): class ChangelogFetcher(QRunnable):
def __init__(self, helper): def __init__(self, helper):
QRunnable.__init__(self) QRunnable.__init__(self)
@ -94,16 +97,16 @@ class Helper(QObject):
pass pass
elif data[:3] == "LPF": elif data[:3] == "LPF":
# More recent version of LogarithmPlotter file, but incompatible with the current format # 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: else:
raise Exception("Invalid LogarithmPlotter file.") raise Exception("Invalid LogarithmPlotter file.")
except Exception as e: # If file can't be loaded except Exception as e: # If file can't be loaded
QMessageBox.warning(None, 'LogarithmPlotter', msg = QCoreApplication.translate('main', 'Could not open file "{}":\n{}')
QCoreApplication.translate('main', 'Could not open file "{}":\n{}').format(filename, QMessageBox.warning(None, 'LogarithmPlotter', msg.format(filename, e), QMessageBox.Ok) # Cannot parse file
e),
QMessageBox.Ok) # Cannot parse file
else: 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: try:
chdir(path.dirname(path.realpath(__file__))) chdir(path.dirname(path.realpath(__file__)))
except NotADirectoryError as e: except NotADirectoryError as e:
@ -131,7 +134,6 @@ class Helper(QObject):
@Slot(str, result=float) @Slot(str, result=float)
def getSettingInt(self, namespace): def getSettingInt(self, namespace):
print('Getting', namespace, config.getSetting(namespace))
return config.getSetting(namespace) return config.getSetting(namespace)
@Slot(str, result=bool) @Slot(str, result=bool)
@ -159,9 +161,8 @@ class Helper(QObject):
""" """
Returns the version info about Qt, PySide6 & Python Returns the version info about Qt, PySide6 & Python
""" """
return QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}").format(PySide6_version, msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}")
sys_version.split( return msg.format(PySide6_version, sys_version.split("\n")[0])
"\n")[0])
@Slot() @Slot()
def fetchChangelog(self): def fetchChangelog(self):

View file

@ -18,11 +18,13 @@
from PySide6.QtQml import QJSValue from PySide6.QtQml import QJSValue
class InvalidAttributeValueException(Exception): pass
class PyJSValue: class PyJSValue:
""" """
Wrapper to provide easy way to interact with JavaScript values in Python directly. Wrapper to provide easy way to interact with JavaScript values in Python directly.
""" """
def __init__(self, js_value: QJSValue, parent: QJSValue = None): def __init__(self, js_value: QJSValue, parent: QJSValue = None):
self.qjs_value = js_value self.qjs_value = js_value
self._parent = parent self._parent = parent
@ -37,16 +39,32 @@ class PyJSValue:
elif isinstance(value, PyJSValue): elif isinstance(value, PyJSValue):
# Set property # Set property
self.qjs_value.setProperty(key, value.qjs_value) self.qjs_value.setProperty(key, value.qjs_value)
else: elif isinstance(value, QJSValue):
print('Setting', key, value)
self.qjs_value.setProperty(key, value) 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): def __call__(self, *args, **kwargs):
value = None
if self.qjs_value.isCallable(): if self.qjs_value.isCallable():
if self._parent is None: if self._parent is None:
return self.qjs_value.call(args) value = self.qjs_value.call(args)
else: else:
return self.qjs_value.callWithInstance(self._parent, args) value = self.qjs_value.callWithInstance(self._parent, args)
else: 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

View file

@ -66,11 +66,11 @@ class Latex(QObject):
self.tempdir = tempdir self.tempdir = tempdir
@Property(bool) @Property(bool)
def latexSupported(self): def latexSupported(self) -> bool:
return LATEX_PATH is not None and DVIPNG_PATH is not None return LATEX_PATH is not None and DVIPNG_PATH is not None
@Slot(result=bool) @Slot(result=bool)
def checkLatexInstallation(self): def checkLatexInstallation(self) -> bool:
""" """
Checks if the current latex installation is valid. Checks if the current latex installation is valid.
""" """
@ -78,19 +78,22 @@ class Latex(QObject):
if LATEX_PATH is None: if LATEX_PATH is None:
print("No Latex installation found.") print("No Latex installation found.")
if "--test-build" not in argv: if "--test-build" not in argv:
QMessageBox.warning(None, "LogarithmPlotter - Latex setup", msg = QCoreApplication.translate("latex",
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/.")) "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 valid_install = False
elif DVIPNG_PATH is None: elif DVIPNG_PATH is None:
print("DVIPNG not found.") print("DVIPNG not found.")
if "--test-build" not in argv: 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 valid_install = False
else: else:
try: try:
self.render("", 14, QColor(0, 0, 0, 255)) self.render("", 14, QColor(0, 0, 0, 255))
except Exception as e: 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 return valid_install
@Slot(str, float, QColor, result=str) @Slot(str, float, QColor, result=str)
@ -121,7 +124,7 @@ class Latex(QObject):
return f'{export_path}.png,{img.width()},{img.height()}' return f'{export_path}.png,{img.width()},{img.height()}'
@Slot(str, float, QColor, result=str) @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. Finds a prerendered image and returns its data if possible, and an empty string if not.
""" """
@ -132,7 +135,6 @@ class Latex(QObject):
data = f'{export_path}.png,{img.width()},{img.height()}' data = f'{export_path}.png,{img.width()},{img.height()}'
return data return data
def create_export_path(self, latex_markup: str, font_size: float, color: QColor): def create_export_path(self, latex_markup: str, font_size: float, color: QColor):
""" """
Standardizes export path for renders. Standardizes export path for renders.
@ -141,7 +143,6 @@ class Latex(QObject):
export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}') export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}')
return markup_hash, export_path return markup_hash, export_path
def create_latex_doc(self, export_path: str, latex_markup: str): def create_latex_doc(self, export_path: str, latex_markup: str):
""" """
Creates a temporary latex document with base file_hash as file name and a given expression markup latex_markup. Creates a temporary latex document with base file_hash as file name and a given expression markup latex_markup.
@ -183,16 +184,18 @@ class Latex(QObject):
""" """
Runs a subprocess and handles exceptions and messages them to the user. 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) proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir.name)
try: try:
out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long. out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long.
if proc.returncode != 0: if proc.returncode != 0:
# Process errored # Process errored
output = str(out, 'utf8') + "\n" + str(err, 'utf8') output = str(out, 'utf8') + "\n" + str(err, 'utf8')
QMessageBox.warning(None, "LogarithmPlotter - Latex", msg = QCoreApplication.translate("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.") "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)) msg = msg.format(cmd, 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'))) 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: except TimeoutExpired as e:
# Process timed out # Process timed out
proc.kill() proc.kill()
@ -202,14 +205,14 @@ class Latex(QObject):
for pkg in PACKAGES: for pkg in PACKAGES:
if f'{pkg}.sty' in output: if f'{pkg}.sty' in output:
# Package missing. # Package missing.
QMessageBox.warning(None, "LogarithmPlotter - Latex", msg = QCoreApplication.translate("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.") "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)) QMessageBox.warning(None, "LogarithmPlotter - Latex", msg.format(pkg, pkg))
raise Exception("Latex: Missing package " + pkg) raise Exception("Latex: Missing package " + pkg)
QMessageBox.warning(None, "LogarithmPlotter - Latex", msg = QCoreApplication.translate("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.") "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)) QMessageBox.warning(None, "LogarithmPlotter - Latex", msg.format(cmd, output))
raise Exception(" ".join(process) + " process timed out:\n" + output) raise Exception(f"{cmd} process timed out:\n{output}")
def cleanup(self, export_path): def cleanup(self, export_path):
""" """

View file

@ -53,9 +53,8 @@ class UpdateCheckerRunnable(QRunnable):
current_version_tuple = self.current_version.split(".") current_version_tuple = self.current_version.split(".")
is_version_newer = version_tuple > current_version_tuple is_version_newer = version_tuple > current_version_tuple
if is_version_newer: if is_version_newer:
msg_text = QCoreApplication.translate("update", msg_text = QCoreApplication.translate("update", "An update for LogarithmPlotter (v{}) is available.")
"An update for LogarithPlotter (v{}) is available.").format( msg_text = msg_text.format(version)
version)
update_available = True update_available = True
else: else:
show_alert = False show_alert = False
@ -63,11 +62,11 @@ class UpdateCheckerRunnable(QRunnable):
except HTTPError as e: except HTTPError as e:
msg_text = QCoreApplication.translate("update", msg_text = QCoreApplication.translate("update",
"Could not fetch update information: Server error {}.").format( "Could not fetch update information: Server error {}.")
str(e.code)) msg_text = msg_text.format(str(e.code))
except URLError as e: except URLError as e:
msg_text = QCoreApplication.translate("update", "Could not fetch update information: {}.").format( msg_text = QCoreApplication.translate("update", "Could not fetch update information: {}.")
str(e.reason)) msg_text = msg_text.format(str(e.reason))
self.callback.got_update_info.emit(show_alert, msg_text, update_available) self.callback.got_update_info.emit(show_alert, msg_text, update_available)

View file

@ -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. - To build the snap, you need [snapcraft](https://snapcraft.io) installed.
- Run `package-linux.sh`. - Run `package-linux.sh`.
### Linux
Run `bash linux/install_local.sh`
## Contribute ## Contribute
There are several ways to contribute to LogarithmPlotter. 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). - 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 ## Legal notice
LogarithmPlotter - 2D plotter software to make BODE plots, sequences and repartition functions. LogarithmPlotter - 2D plotter software to make BODE plots, sequences and repartition functions.
Copyright (C) 2021-2024 Ad5001 <mail@ad5001.eu> Copyright (C) 2021-2024 Ad5001 <mail@ad5001.eu>

View file

@ -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()

View file

View file