diff --git a/README.md b/README.md index c4b0761..57b418e 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,17 @@ This tool is a fork from upstream canfestival-3-asc repo: > https://github.com/Laerdal/canfestival-3-asc +## Making objdictedit excutable + +To be able build an executable that can be run from anywhere: + + $ pip install pyinstaller + $ pyinstaller packaging/objdictedit.spec + +The file `dist/objdictedit.exe` can now be used anywhere. It does not need any +pre-installed software. + + ## License Objdictgen has been based on the python tool included in CanFestival. This diff --git a/packaging/filereplacer.py b/packaging/filereplacer.py new file mode 100644 index 0000000..33d88ba --- /dev/null +++ b/packaging/filereplacer.py @@ -0,0 +1,56 @@ + +import os +import re +import warnings +import objdictgen +from setuptools import SetuptoolsDeprecationWarning +from setuptools.config import read_configuration + +warnings.filterwarnings("ignore", category=SetuptoolsDeprecationWarning) + + +def convert(infile, outfile): + ''' Tool to replace @@{VAR_NAME} in files.''' + + pat = re.compile(r'^(.*?)@@{(.[^}]+)}(.*)$', re.S) + + config = read_configuration('setup.cfg')["metadata"] + + # Some hacks + config['version_tuple'] = objdictgen.__version_tuple__ + config['copyright'] = objdictgen.__copyright__ + + with open(infile, "r", encoding="utf-8") as fin: + out = '' + for line in fin: + + while True: + m = pat.fullmatch(line) + if not m: + out += line + break + + out += m[1] + name = m[2] + line = m[3] # Remainder for the next iteration + if name in config: + out += str(config[m[2]]) + + elif name in os.environ: + out += os.environ[name] + + else: + raise KeyError(f"The variable {name} is not defined") + + with open(outfile, 'w', encoding="utf-8") as fout: + fout.write(out) + + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('input', help='Input file') + parser.add_argument('output', help='Output file') + args = parser.parse_args() + + convert(args.input, args.output) diff --git a/packaging/objdictedit.spec b/packaging/objdictedit.spec new file mode 100644 index 0000000..679e969 --- /dev/null +++ b/packaging/objdictedit.spec @@ -0,0 +1,63 @@ +# -*- mode: python ; coding: utf-8 -*- +import os +import sys +sys.path.append("packaging") +from filereplacer import convert + +basepath = os.path.dirname(os.path.dirname(os.path.abspath(SPEC))) + +workpath = basepath +os.chdir(workpath) + +script = """ +from objdictgen.ui import objdictedit +objdictedit.main() +""" + +os.makedirs("build", exist_ok=True) +with open("build/objdictedit.py", "w", encoding="utf-8") as f: + f.write(script) + +icon = basepath + "/src/objdictgen/img/networkedit.ico" + +convert("packaging/objdictedit.ver.in", "build/objdictedit.ver") + +a = Analysis( + [basepath + '/build/objdictedit.py'], + pathex=[], + binaries=[], + datas=[ + (basepath + "/src/objdictgen/img/networkedit.ico", "objdictgen/img"), + ], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='objdictedit', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=icon, + version=basepath + '/build/objdictedit.ver', +) diff --git a/packaging/objdictedit.ver.in b/packaging/objdictedit.ver.in new file mode 100644 index 0000000..c90b9f1 --- /dev/null +++ b/packaging/objdictedit.ver.in @@ -0,0 +1,43 @@ +# UTF-8 +# +# For more details about fixed file info 'ffi' see: +# http://msdn.microsoft.com/en-us/library/ms646997.aspx +VSVersionInfo( + ffi=FixedFileInfo( +# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) +# Set not needed items to zero 0. +filevers=@@{version_tuple}, +prodvers=@@{version_tuple}, +# Contains a bitmask that specifies the valid bits 'flags'r +mask=0x3f, +# Contains a bitmask that specifies the Boolean attributes of the file. +flags=0x0, +# The operating system for which this file was designed. +# 0x4 - NT and there is no need to change it. +OS=0x4, +# The general type of file. +# 0x1 - the file is an application. +fileType=0x1, +# The function of the file. +# 0x0 - the function is not defined for this fileType +subtype=0x0, +# Creation date and time stamp. +date=(0, 0) +), + kids=[ +StringFileInfo( + [ + StringTable( + u'040904B0', + [StringStruct(u'CompanyName', u'Object Dictionary Editor'), + StringStruct(u'FileDescription', u'@@{description}'), + StringStruct(u'FileVersion', u'@@{version}'), + StringStruct(u'InternalName', u'objdictedit'), + StringStruct(u'LegalCopyright', u'@@{copyright}'), + StringStruct(u'OriginalFilename', u'objdictedit.exe'), + StringStruct(u'ProductName', u'objdictedit'), + StringStruct(u'ProductVersion', u'@@{version}')]) + ]), +VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) + ] +) diff --git a/sonar-project.properties b/sonar-project.properties index 98f47b1..741820f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,4 @@ sonar.projectKey=Laerdal_python-objdictgen sonar.organization=laerdal-foss sonar.python.coverage.reportPaths=coverage.xml -sonar.exclusions = tests/** +sonar.exclusions = tests/**, packaging/** diff --git a/src/objdictgen/__init__.py b/src/objdictgen/__init__.py index 5c79aed..908e565 100644 --- a/src/objdictgen/__init__.py +++ b/src/objdictgen/__init__.py @@ -24,6 +24,8 @@ from objdictgen.nodemanager import NodeManager __version__ = "3.5.1a1" +__version_tuple__ = (3, 5, 1, 1) +__copyright__ = "(c) 2024 Svein Seldal, Laerdal Medical AS, and several. Licensed under GPLv2.1." # Shortcuts LoadFile = Node.LoadFile diff --git a/src/objdictgen/ui/objdictedit.py b/src/objdictgen/ui/objdictedit.py index 0579f24..bb3b2df 100644 --- a/src/objdictgen/ui/objdictedit.py +++ b/src/objdictgen/ui/objdictedit.py @@ -197,9 +197,9 @@ def _init_utils(self): def _init_ctrls(self, parent): wx.Frame.__init__( - self, id=ID_OBJDICTEDIT, name='objdictedit', + self, id=ID_OBJDICTEDIT, name=self.title, parent=parent, pos=wx.Point(149, 178), size=wx.Size(1000, 700), - style=wx.DEFAULT_FRAME_STYLE, title='Objdictedit', + style=wx.DEFAULT_FRAME_STYLE, title=self.title, ) self._init_utils() self.SetClientSize(wx.Size(1000, 700)) @@ -229,6 +229,8 @@ def _init_ctrls(self, parent): self.SetStatusBar(self.HelpBar) def __init__(self, parent, manager: NodeManager|None = None, filesopen: list[TPath]|None = None): + self.title = f"Object dictionary editor v{objdictgen.__version__}" + filesopen = filesopen or [] if manager is None: NodeEditorTemplate.__init__(self, NodeManager(), True) @@ -323,9 +325,9 @@ def OnCloseFrame(self, event): def RefreshTitle(self): if self.FileOpened.GetPageCount() > 0: - self.SetTitle(f"Objdictedit - {self.Manager.GetCurrentFilename()}") + self.SetTitle(self.title + f" - {self.Manager.GetCurrentFilename()}") else: - self.SetTitle("Objdictedit") + self.SetTitle(self.title) def RefreshCurrentIndexList(self): selected = self.FileOpened.GetSelection() diff --git a/tests/test_installer.py b/tests/test_installer.py new file mode 100644 index 0000000..c71a40c --- /dev/null +++ b/tests/test_installer.py @@ -0,0 +1,38 @@ + +import pytest +from unittest import mock +import os +import sys + +@pytest.fixture +def setenvvar(monkeypatch): + with mock.patch.dict(os.environ, {"TEST": "foobar"}): + yield + + +def test_filereplacer(basepath, wd, setenvvar): + + sys.path.append(str(basepath / "packaging")) + os.chdir(basepath) + from filereplacer import convert + + tests = [ + (1, "Test data", "Test data"), + (2, "@@{name}", "objdictgen"), + (3, "@@{TEST}", "foobar"), # Read from the mocked environment variable + (4, "@@{nonexisting}", "non-existing"), + ] + + for i, data, result in tests: + infile = wd / "test.txt" + outfile = wd / "out.txt" + with open(infile, "w", encoding="utf-8") as f: + f.write(data) + if i == 4: + with pytest.raises(KeyError): + convert(infile, outfile) + continue + else: + convert(infile, outfile) + with open(outfile, "r", encoding="utf-8") as f: + assert f.read() == result