Commit 63a54b5d authored by eckhart's avatar eckhart

Merge branch 'development' of gitlab.lrz.de:badw-it/DHParser into development

parents 99e401cc d3009b08
......@@ -28,7 +28,6 @@ DevScripts/DHParser.py
DHParser/cstringview.c
*.so
.mypy_cache
.vscode/
DHParser.egg-info
.noseids/
*.build
......@@ -42,7 +41,6 @@ _build
_build
_static
_templates
.vs
OLDSTUFF
.pytest_cache
*.c
......
......@@ -102,7 +102,8 @@ class Compiler:
context: A list of parent nodes that ends with the currently
compiled node.
tree: The root of the abstract syntax tree.
source: The source code.
finalizers: A stack of tuples (function, parameters) that will be
called in reverse order after compilation.
_dirty_flag: A flag indicating that the compiler has already been
called at least once and that therefore all compilation
......@@ -120,12 +121,28 @@ class Compiler:
def reset(self):
# self.source = ''
self.finlizers = [] # type: List[Callable, Tuple]
self.tree = ROOTNODE_PLACEHOLDER # type: RootNode
self.context = [] # type: List[Node]
self._None_check = True # type: bool
self._dirty_flag = False
self._debug = get_config_value('debug') # type: bool
self._debug_already_compiled = set() # type: Set[Node]
self.finalizers = [] # type: List[Callable, Tuple]
def prepare(self) -> None:
"""
A preparation method that will be called after everything else has
been initialized and immediately before compilation starts. This method
can be overwritten in order to implement preparation tasks.
"""
pass
def finalize(self) -> None:
"""
A finalization method that is called after compilation has finished and
after all tasks from the finalizers stack have been executed
"""
def __call__(self, root: RootNode) -> Any:
"""
......@@ -141,7 +158,11 @@ class Compiler:
self._dirty_flag = True
self.tree = root # type: RootNode
# self.source = source # type: str
self.prepare()
result = self.compile(root)
while self.finalizers:
task, parameters = self.finalizers.pop()
task(*parameters)
return result
# Obsolete, because never used...
......
......@@ -86,6 +86,7 @@ class Error:
PARSER_DID_NOT_MATCH = ErrorCode(1020)
PARSER_LOOKAHEAD_MATCH_ONLY = ErrorCode(1030)
PARSER_STOPPED_BEFORE_END = ErrorCode(1040)
PARSER_STOPPED_EXCEPT_FOR_LOOKAHEAD = ErrorCode(1045)
CAPTURE_STACK_NOT_EMPTY = ErrorCode(1050)
MALFORMED_ERROR_STRING = ErrorCode(1060)
AMBIGUOUS_ERROR_HANDLING = ErrorCode(1070)
......
......@@ -974,7 +974,9 @@ class Grammar:
result, 'Parser "%s" did not match empty document.' % str(parser),
Error.PARSER_DID_NOT_MATCH)
while rest and len(stitches) < self.max_parser_dropouts__:
# copy to local variable, so break condition can be triggered manually
max_parser_dropouts = self.max_parser_dropouts__
while rest and len(stitches) < max_parser_dropouts:
result, rest = parser(rest)
if rest:
fwd = rest.find("\n") + 1 or len(rest)
......@@ -994,13 +996,21 @@ class Grammar:
error_code = Error.PARSER_DID_NOT_MATCH
else:
stitches.append(result)
error_msg = "Parser stopped before end" \
+ (("! trying to recover"
+ (" but stopping history recording at this point."
if self.history_tracking__ else "..."))
if len(stitches) < self.max_parser_dropouts__
else " too often! Terminating parser.")
error_code = Error.PARSER_STOPPED_BEFORE_END
h = self.history__[-1] if self.history__ else \
HistoryRecord([], None, StringView(''), (0, 0))
if h.status == h.MATCH and (h.node.pos + len(h.node) == len(self.document__)):
# TODO: this case still needs unit-tests and support in testing.py
error_msg = "Parser stopped before end, but matched with lookahead."
error_code = Error.PARSER_STOPPED_EXCEPT_FOR_LOOKAHEAD
max_parser_dropouts = -1 # no further retries!
else:
error_msg = "Parser stopped before end" \
+ (("! trying to recover"
+ (" but stopping history recording at this point."
if self.history_tracking__ else "..."))
if len(stitches) < self.max_parser_dropouts__
else " too often! Terminating parser.")
error_code = Error.PARSER_STOPPED_BEFORE_END
stitches.append(Node(ZOMBIE_TAG, skip).with_pos(tail_pos(stitches)))
self.tree__.new_error(stitches[-1], error_msg, error_code)
if self.history_tracking__:
......
......@@ -44,12 +44,14 @@ For JSON see:
# TODO: Test with python 3.5
import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, CancelledError
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, CancelledError, \
BrokenExecutor
import json
from multiprocessing import Process, Queue, Value, Array
import sys
import time
from typing import Callable, Coroutine, Optional, Union, Dict, List, Tuple, Sequence, Set, Any, cast
from typing import Callable, Coroutine, Optional, Union, Dict, List, Tuple, Sequence, Set, Any, \
cast
from DHParser.syntaxtree import DHParser_JSONEncoder
from DHParser.toolkit import get_config_value, re
......@@ -255,8 +257,10 @@ class Server:
rpc_error = -32602, "Invalid Params: " + str(e)
except NameError as e:
rpc_error = -32601, "Method not found: " + str(e)
except BrokenExecutor as e:
rpc_error = -32000, "Broken Executor: " + str(e)
except Exception as e:
rpc_error = -32000, "Server Error: " + str(e)
rpc_error = -32000, "Server Error " + str(type(e)) + ': ' + str(e)
if data.startswith(b'GET'):
# HTTP request
......
......@@ -123,8 +123,33 @@ def flatten_xml(xml: str) -> str:
return re.sub(r'\s+(?=<[\w:])', '', re.sub(r'(?P<closing_tag></:?\w+>)\s+', tag_only, xml))
# criteria for finding nodes:
# - node itself (equality)
# - tag_name
# - one of several tag_names
# - a function Node -> bool
CriteriaType = Union['Node', str, Container[str], Callable]
def create_match_function(criterion: CriteriaType) -> Callable:
"""
Returns a valid match function (Node -> bool) for the given criterion.
Match functions are used to find an select particular nodes from a
tree of nodes.
"""
if isinstance(criterion, Node):
return lambda nd: nd == criterion
elif isinstance(criterion, str):
return lambda nd: nd.tag_name == criterion
elif isinstance(criterion, Container):
return lambda nd: nd.tag_name in criterion
if isinstance(criterion, Callable):
return criterion
assert "Criterion %s of type %s does not represent a legal criteria type"
ChildrenType = Tuple['Node', ...]
NoChildren = cast(ChildrenType, ()) # type: ChildrenType
NO_CHILDREN = cast(ChildrenType, ()) # type: ChildrenType
StrictResultType = Union[ChildrenType, StringView, str]
ResultType = Union[ChildrenType, 'Node', StringView, str, None]
......@@ -190,7 +215,7 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
# The following if-clause is merely an optimization, i.e. a fast-path for leaf-Nodes
if leafhint:
self._result = result # type: StrictResultType # cast(StrictResultType, result)
self.children = NoChildren # type: ChildrenType
self.children = NO_CHILDREN # type: ChildrenType
else:
self.result = result
self.tag_name = tag_name # type: str
......@@ -309,7 +334,7 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
self.children = result
self._result = result or ''
else:
self.children = NoChildren
self.children = NO_CHILDREN
self._result = result # cast(StrictResultType, result)
def _content(self) -> List[str]:
......@@ -459,7 +484,7 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
# tree traversal and node selection ###
def __getitem__(self, index_or_tagname: Union[int, str]) -> Union['Node', Iterator['Node']]:
def __getitem__(self, key: Union[CriteriaType, int]) -> Union['Node', Iterator['Node']]:
"""
Returns the child node with the given index if ``index_or_tagname`` is
an integer or the first child node with the given tag name. Examples::
......@@ -471,57 +496,69 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
'(X (c "d"))'
Args:
index_or_tagname(str): Either an index of a child node or a
tag name.
key(str): A criterion (tag name(s), match function, node) or
an index of the child that shall be returned.
Returns:
Node: All nodes which have a given tag name.
Raises:
KeyError: if no matching child was found.
IndexError: if key was an integer index that did not exist
ValueError: if the __getitem__ has been called on a leaf node.
"""
if self.children:
if isinstance(index_or_tagname, int):
return self.children[index_or_tagname]
if isinstance(key, int):
return self.children[key]
else:
mf = create_match_function(cast(CriteriaType, key))
for child in self.children:
if child.tag_name == index_or_tagname:
if mf(child):
return child
raise KeyError(index_or_tagname)
raise ValueError('Leave nodes have no children that can be indexed!')
raise KeyError(str(key))
raise ValueError('Leaf-nodes have no children that can be indexed!')
def __contains__(self, tag_name: str) -> bool:
def __contains__(self, what: CriteriaType) -> bool:
"""
Returns true if a child with the given tag name exists.
Args:
tag_name (str): tag_name which will be searched among to immediate
descendants of this node.
what: a criterion that describes what (kind of) child node
shall be looked for among the children of the node. This
can a node, a tag name or collection of tag names or an
arbitrary matching function (Node -> bool).
Returns:
bool: True, if at least one descendant node with the given tag
name exists, False otherwise
bool: True, if at least one child which fulfills the criterion
exists
"""
# assert isinstance(tag_name, str)
if self.children:
mf = create_match_function(what)
for child in self.children:
if child.tag_name == tag_name:
if mf(child):
return True
return False
raise ValueError('Leave node cannot contain other nodes')
raise ValueError('Leaf-node cannot contain other nodes')
def index(self, tag_name: str, start: int = 0, stop: int = sys.maxsize) -> int:
def index(self, what: CriteriaType, start: int = 0, stop: int = sys.maxsize) -> int:
"""
Returns the first index of the child with the tag name `what`. If the
parameters start and stop are given, the search is restricted to the
children with indices from the half-open interval [start:end[.
If no such child exists a ValueError is raised.
:param tag_name: the child's tag name for which the index shall be returned
Returns the first index of the child that fulfills the criteriuon
`what`. If the parameters start and stop are given, the search is
restricted to the children with indices from the half-open interval
[start:end[. If no such child exists a ValueError is raised.
:param what: the criterion by which the child is identified, the index
of which shall be returned.
:param start: the first index to start searching.
:param stop: the last index that shall be searched
:return: the index of the first child with the given tag name.
:raises: ValueError, if no child matching the criterion `what` was found.
"""
assert 0 <= start <= stop
assert 0 <= start < stop
i = start
mf = create_match_function(what)
for child in self.children[start:stop]:
if child.tag_name == tag_name:
if mf(child):
return i
i += 1
raise ValueError("Node with tag name '%s' not among child-nodes." % tag_name)
if i >= stop:
break
raise ValueError("Node identified by '%s' not among child-nodes." % str(what))
def select_if(self, match_function: Callable, include_root: bool = False, reverse: bool = False) \
-> Iterator['Node']:
......@@ -537,7 +574,7 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
yield child
yield from child.select_if(match_function, False, reverse)
def select(self, criterion: Union[str, Container[str], Callable],
def select(self, criterion: CriteriaType,
include_root: bool = False, reverse: bool = False) -> Iterator['Node']:
"""
Finds nodes in the tree that fulfill a given criterion. This criterion
......@@ -551,15 +588,15 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
See function `Node.select` for some examples.
Args:
criterion: A function that takes as Node
object as argument and returns True or False
criterion: A "criterion" for identifying the nodes to be selected.
This can either be a tag name (string), a collection of
tag names or a match function (Node -> bool)
include_root (bool): If False, only descendant nodes will be
checked for a match.
reverse (bool): If True, the tree will be walked in reverse
order, i.e. last children first.
Yields:
Node: All nodes of the tree for which
``match_function(node)`` returns True
Node: All nodes of the tree which fulfill the given criterion
Examples::
......@@ -574,24 +611,10 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
['(a (b "X") (X (c "d")) (e (X "F")))']
>>> flatten_sxpr(next(tree.select("X", False)).as_sxpr())
'(X (c "d"))'
Args:
tag_names(set): A tag name or set of tag names that is being
searched for
include_root (bool): If False, only descendant nodes will be
checked for a match.
Yields:
Node: All nodes which have a given tag name.
"""
if isinstance(criterion, str):
return self.select_if(lambda node: node.tag_name == criterion, include_root, reverse)
elif isinstance(criterion, Container):
return self.select_if(lambda node: node.tag_name in criterion, include_root, reverse)
else:
assert isinstance(criterion, Callable)
return self.select_if(criterion, include_root, reverse)
return self.select_if(create_match_function(criterion), include_root, reverse)
def pick(self, criterion: Union[str, Container[str], Callable],
def pick(self, criterion: CriteriaType,
reverse: bool = False) -> Optional['Node']:
"""
Picks the first (or last if run in reverse mode) descendant that fulfills
......@@ -998,7 +1021,7 @@ class RootNode(Node):
duplicate.children = copy.deepcopy(self.children)
duplicate._result = duplicate.children
else:
duplicate.children = NoChildren
duplicate.children = NO_CHILDREN
duplicate._result = self._result
duplicate._pos = self._pos
if self.has_attr():
......
......@@ -342,7 +342,7 @@ def grammar_unit(test_unit, parser_factory, transformer_factory, report=True, ve
parser[parser_name].apply(find_lookahead)
return lookahead_found
def lookahead_artifact(st):
def lookahead_artifact(syntax_tree: Node):
"""
Returns True, if the error merely occurred, because the parser
stopped in front of a sequence that was captured by a lookahead
......@@ -350,19 +350,22 @@ def grammar_unit(test_unit, parser_factory, transformer_factory, report=True, ve
This is required for testing of parsers that put a lookahead
operator at the end. See test_testing.TestLookahead.
"""
raw_errors = st.errors_sorted
is_artifact = ((len(raw_errors) == 2 # case 1: superfluous data for lookahead
and raw_errors[-1].code == Error.PARSER_LOOKAHEAD_MATCH_ONLY
and raw_errors[-2].code == Error.PARSER_STOPPED_BEFORE_END)
# case 2: mandatory lookahead failure at end of text
raw_errors = syntax_tree.errors_sorted
is_artifact = ((2 <= len(raw_errors) == 3 # case 1: superfluous data for lookahead
and {e.code for e in raw_errors} <=
{Error.PARSER_LOOKAHEAD_MATCH_ONLY,
# Error.PARSER_STOPPED_BEFORE_END,
Error.PARSER_STOPPED_EXCEPT_FOR_LOOKAHEAD})
or (len(raw_errors) == 1
and raw_errors[-1].code == Error.MANDATORY_CONTINUATION_AT_EOF))
and (raw_errors[-1].code == Error.PARSER_STOPPED_EXCEPT_FOR_LOOKAHEAD
# case 2: mandatory lookahead failure at end of text
or raw_errors[-1].code == Error.MANDATORY_CONTINUATION_AT_EOF)))
if is_artifact:
# don't remove zombie node with error message at the end
# but change it's tag_name to indicate that it is an artifact!
for parent in st.select_if(lambda node: any(child.tag_name == ZOMBIE_TAG
for child in node.children),
include_root=True, reverse=True):
for parent in syntax_tree.select_if(lambda node: any(child.tag_name == ZOMBIE_TAG
for child in node.children),
include_root=True, reverse=True):
zombie = parent[ZOMBIE_TAG]
zombie.tag_name = '__TESTING_ARTIFACT__'
zombie.result = 'Artifact can be ignored. Be aware, though, that also the' \
......@@ -472,7 +475,7 @@ def grammar_unit(test_unit, parser_factory, transformer_factory, report=True, ve
cst = RootNode(node).new_error(node, str(upe))
errata.append('Unknown parser "{}" in fail test "{}"!'.format(parser_name, test_name))
tests.setdefault('__err__', {})[test_name] = errata[-1]
if not is_error(cst.error_flag) and not lookahead_artifact(cst):
if not (is_error(cst.error_flag) and not lookahead_artifact(cst)):
errata.append('Fail test "%s" for parser "%s" yields match instead of '
'expected failure!' % (test_name, parser_name))
tests.setdefault('__err__', {})[test_name] = errata[-1]
......@@ -850,6 +853,7 @@ def run_path(path):
def clean_report():
"""Deletes any test-report-files in the REPORT sub-directory and removes
the REPORT sub-directory, if it is empty after deleting the files."""
# TODO: make this thread safe, if possible!!!!
if os.path.exists('REPORT'):
files = os.listdir('REPORT')
flag = False
......
#!/usr/bin/python3
"""Lädt das MLW-Installationsskript vonb gitlab herunter und startet es.
Nach dem Starten wird das Installationsskript wieder gelöscht, d.h. um
es wieder zu starten, muss erst dieses Skript wieder ausgeführt werden,
welches das jeweils aktuelle Installationsskript herunterlädt.
"""
import os
import ssl
import sys
import urllib.request
cwd = os.path.abspath(os.getcwd())
if cwd.endswith('MLW-DSL'):
print('Der Pfad "%s" scheint das MLW-Git-Repositorium zu enthalten.' % cwd)
print('Dieses Skript sollte nicht innerhalb des MLW-Git-Repositoriums ausgeführt werden!')
sys.exit(1)
PFAD = "https://gitlab.lrz.de/badw-it/MLW-DSL/raw/master/Installiere-MLW.py"
SKRIPT_NAME = os.path.basename(PFAD)
# Lade Installationsskript herunter
with urllib.request.urlopen(PFAD, context=ssl.create_default_context()) as https:
skript = https.read()
# Speichere Installationsskript im aktuellen Ordner (Desktop)
with open(SKRIPT_NAME, 'wb') as datei:
datei.write(skript)
# Führe Installationsskript aus
os.system('python.exe ' + SKRIPT_NAME)
# Lösche Installationsskript nach der Ausführung, damit nicht versehentlich
# ein veraltetes Installationsskript gestartet wird.
os.remove(SKRIPT_NAME)
#!/usr/bin/python3
"""Installiere-MLW.py - installiert MLW-Erweiterungen für Visual Studio Code
Copyright 2016 by Eckhart Arnold (arnold@badw.de)
Bavarian Academy of Sciences an Humanities (badw.de)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied. See the License for the specific language governing
permissions and limitations under the License.
"""
import datetime
import os
import platform
import re
import shutil
import ssl
import subprocess
import threading
import tkinter as tk
from tkinter import filedialog, messagebox
from typing import Callable
import urllib.request
import zipfile
VSC_MINDEST_VERSION = (1,22,0)
#######################################################################
#
# Beispielartikel: fascitergula
#
#######################################################################
Beispielartikel = """
LEMMA *facitergula
(fasc-, -iet-, -ist-, -rcu-)
{sim.}
GRAMMATIK
nomen; -ae f.
-us, -i m.: {=> faceterculi}
-um, -i n.: {=> facitergulum}
SCHREIBWEISE
script.:
vizreg-: {=> vizregule}
festregel(a): {=> festregelę}
fezdregl(a): {=> fezdreglę}
BEDEUTUNG pannus, faciale, sudarium -- Gesichtstuch, Schweißtuch,
Tuch {usu liturg.}{de re v. {=> eintrag/ibi_X}}:
* CATAL. thes. Germ.; 28,11 (post 851) "#facitergulum III"
* FORM. Sangall.; 39 p. 421,16
"munuscula ... direximus, hoc est palliolum ... , #facitergulas duas"
* CATAL. thes. Germ.; 18,7 "#faceterculi viginti quatuor"
* LIBRI confrat.; III app. A 6 p. 137,30 "pulpitum ... #facitergula
cocco imaginata circumdari iussit {pontifex}"
* CATAL. thes. Germ.; 76,15 "#faciterulae II"; 40,5 VI "#vizregule";
129a,5 "#facisterculas II."; 24,8 "#facitella X"; 114,8 VIII
"#fezdreglę"; 6,24 "#fasciutercule VII"; 92,6 "#fascerculę tres.";
21,20 IIII "#festregelę" {saepe}
BEDEUTUNG capital, rica -- Kopftuch:
* TRANSL. Libor. I; 32
"raptis feminarum #facitergulis (fa[s]citergiis {var. l.})."
* TRANSL. Libor. II; 20
"nuditatem membrorum illius {puellae} tegere festinarunt fideles clerici
et laici inprimis cum eorum #facitercula, dein vestibus solitis."
AUTORIN Weber
"""
#######################################################################
#
# Quell und Zielverzeichnisse (vorgegeben)
#
#######################################################################
def bestimme_vsc_version(kommando):
with subprocess.Popen([kommando, "-v"], stdout=subprocess.PIPE,
bufsize=1, universal_newlines=True) as p:
version_info = [line for line in p.stdout]
if version_info:
version = tuple(int(part) for part in version_info[0].split('.'))
else:
version = (0, 0, 0)
return version
nutzerverzeichnis = os.path.expanduser('~')
heimverzeichnis = 'H:\\MLW-DSL' if os.path.exists('H:') else \
os.path.join(nutzerverzeichnis, 'MLW-DSL')
vsc_ersatzpfad = os.path.join(os.path.expanduser('~'), 'VSCode')
vsc_kommando = 'code'
if platform.system() == "Windows":
vsc_kommando = 'C:\\Program Files\\Microsoft VS Code\\bin\\code.cmd'
alt_vsc_kommando = os.path.join(vsc_ersatzpfad, 'bin', 'code.cmd')
if not os.path.exists(vsc_kommando):
vsc_kommando = 'C:\\Program Files (x86)\\Microsoft VS Code\\bin\\code.cmd'
if not os.path.exists(vsc_kommando):
vsc_kommando = os.path.join(os.path.expanduser('~'),
"AppData\\Local\\Programs\\Microsoft VS Code\\bin\\code.cmd")
if not os.path.exists(vsc_kommando):
vsc_kommando = alt_vsc_kommando
else:
alt_vsc_kommando = os.path.join(vsc_ersatzpfad, 'bin', 'code')
try:
subprocess.run([vsc_kommando, '-v'])
except FileNotFoundError:
vsc_kommando = alt_vsc_kommando
mlw_version = (0, 0, 0)
arbeitsverzeichnis = os.path.join(heimverzeichnis, 'MLW-Artikel')
softwareverzeichnis_name = 'MLW-Software'
softwareverzeichnis = os.path.join(heimverzeichnis, softwareverzeichnis_name)
skriptverzeichnis = os.path.dirname(os.path.realpath(__file__))
archivadresse = "https://gitlab.lrz.de/badw-it/MLW-DSL/repository/master/archive.zip"
dhparser_archivadresse = "https://gitlab.lrz.de/badw-it/DHParser/repository/master/archive.zip"
if platform.system() == "Windows":
vsc_archivadresse = "https://eckhartarnold.de/backup/VSCode_win32.zip"
elif platform.system() == "Linux":
vsc_archivadresse = "https://eckhartarnold.de/backup/VSCode_linux.zip"
else:
vsc_archivadresse = 'kein Archiv mit VSCode hinterlegt!'
vsc_konfig_verzeichnis = os.path.join(os.path.expanduser('~'), '.vscode')
vsc_version = (0, 0, 0)
alt_vsc_version = (0, 0, 0)
try:
vsc_version = bestimme_vsc_version(vsc_kommando)
if vsc_version < VSC_MINDEST_VERSION and alt_vsc_kommando != vsc_kommando:
kommando = alt_vsc_kommando
alt_vsc_version = bestimme_vsc_version(alt_vsc_kommando)
else:
kommando = vsc_kommando
except FileNotFoundError:
kommando = alt_vsc_kommando
vsc_vorhanden = vsc_version != (0, 0, 0)
version = max(vsc_version, alt_vsc_version)
schon_installiert = max(alt_vsc_version, vsc_version) >= VSC_MINDEST_VERSION \
and os.path.exists(softwareverzeichnis)
def lies_versionshinweise(ordner=softwareverzeichnis):
try:
with open(os.path.join(ordner, 'ÄNDERUNGEN.txt'),
'r', encoding='utf-8') as f:
änderungen = f.read()
match = re.search(r'Version\s+(\d+\.\d+\.\d+)(\s*\w*)', änderungen)
versions_str = match.group(0)
versions_tuple = tuple(int(part) for part in match.group(1).split('.'))
match = re.search('-------+\n(.*?)Version', änderungen, flags=re.DOTALL)
versions_hinweise = match.group(1).strip()
return versions_str, versions_tuple, versions_hinweise
except FileNotFoundError:
print('Konnte "ÄNDERUNGEN.txt" nicht finden - schade :-(')
return "unknown", (0, 0, 0), ""
#######################################################################
#
# Installationsroutinen
#
#######################################################################
FortschrittsRückruf = Callable[[str, float], None]
FehlerRückruf = Callable[[str], None]
AbbruchRückruf = Callable[[], bool]
def Installiere_Fremdsoftware(MSG: FortschrittsRückruf,
WARN: FehlerRückruf,
abbruch: AbbruchRückruf=lambda: False):
global vsc_kommando, alt_vsc_kommando, kommando, \
version, vsc_version, alt_vsc_version, vsc_vorhanden
MSG('Prüfe, ob Visual Studio Code installiert und Version aktuell', 0.0)
if vsc_vorhanden:
if vsc_version < VSC_MINDEST_VERSION:
if alt_vsc_kommando != vsc_kommando:
vsc_kommando = alt_vsc_kommando
if version < VSC_MINDEST_VERSION:
WARN('VERALTETE Visual Studio Code Version %i.%i.%i gefunden!\n' % version
+ 'Als Notlösung wird eine neuere Version parallel installiert.', 0.1)
vsc_vorhanden = False
else:
MSG('Aktuelle Visual Studio Code Version %i.%i.%i gefunden' % version, 0.1)
else:
MSG('Visual Studio Code nicht gefunden!', 0.1)
if abbruch():
return
if not vsc_vorhanden:
MSG('NOTLÖSUNG: Visual Studio Code ToGo-Installation.')
if os.path.exists(vsc_ersatzpfad):
if os.path.exists(alt_vsc_kommando) or not os.listdir(vsc_ersatzpfad):
MSG('Lösche altes Visual Studio Code ToGo', 0.2)