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__/
.ropeproject
.vscode
*.kdev4
.kdev4
.coverage
build
docs/html
.directory
*.kdev4
*.lpf
*.lgg
*.spec
.kdev4
AccountFree.pro
AccountFree.pro.user
*.egg-info/
*.tar.gz

View file

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

View file

@ -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"):

View file

@ -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,
@ -54,21 +53,27 @@ 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):
@ -80,14 +85,16 @@ def init():
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

View file

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

View file

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

View file

@ -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,13 +78,16 @@ 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:
@ -121,7 +124,7 @@ class Latex(QObject):
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.
"""
@ -132,7 +135,6 @@ class Latex(QObject):
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.
@ -141,7 +143,6 @@ class Latex(QObject):
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):
"""
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.
"""
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):
"""

View file

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

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.
- 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 <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