Commit 04d9ee86 authored by Adrian Wuillemet's avatar Adrian Wuillemet
Browse files

#63 #55 Fix code after merge

parent 95f270f8
......@@ -2,6 +2,7 @@ from meta import *
from modules import defaults, cli, gui as gui_handler
from modules.clicklog import Overview, info, warn
from typing import IO
import datetime
import logging
import click
import os
......@@ -57,13 +58,13 @@ def gui(no_window: bool, port: int, is_verbose: bool, file: IO) -> None:
@beads.command()
@click.option('-l', '--language', type=click.Choice(supported_languages), default=None, help=help_languages)
@click.option('-nv', '--no-validation', flag_value=True, help=help_no_validation)
@click.option('-o', '--out-dir', type=click.Path(exists=True, file_okay=False), default=None, help=help_out_dir)
@click.option('-f', '--file-name', type=str, default=None, help=help_compile_name)
@click.option('-o', '--out', 'file_name', type=str, default=None, help=help_compile_name)
@click.option('-bd', '--base-directory', type=click.Path(exists=True, file_okay=False), default=None, help=help_out_dir)
@click.option('-re', '--replace-existing', 'replace', flag_value=True, help=help_replace_existing)
@click.option('-v', '--verbose', 'is_verbose', flag_value=True, help=help_verbose)
@click.argument('file', type=click.File(mode='r'))
def parse(file: IO, language: str, no_validation: bool, is_verbose: bool,
out_dir: str, file_name: str, replace: bool) -> None:
@click.argument('files', type=click.File(mode='r'), nargs=-1, required=True)
def parse(files: list, language: str, no_validation: bool, is_verbose: bool,
base_directory: str, file_name: str, replace: bool) -> None:
"""
Parse a provided FILE into code.
......@@ -71,21 +72,28 @@ def parse(file: IO, language: str, no_validation: bool, is_verbose: bool,
and generate a code snippet representing this machines internal logic.
"""
set_up_logger(is_verbose)
opts = dict()
opts[SKIP_VALIDATION] = no_validation or defaults.get(SKIP_VALIDATION, False)
opts[REPLACE] = replace or defaults.get(REPLACE, False)
opts[OUT_DIR] = out_dir or defaults.get(OUT_DIR, os.getcwd())
opts[OUT_DIR] = base_directory or defaults.get(OUT_DIR, os.getcwd())
opts[FILENAME] = file_name or file.name.partition('.')[0]
language = language or defaults.get(LANGUAGE, None)
set_up_logger(is_verbose)
if len(files) == 1:
file = files[0]
opts[FILENAME] = file_name or default_file_name(file)
for file in files:
opts[OUT_FILE] = f'{out_file}_{1 + files.index(file)}' or file.name.partition('.')[0]
logging.debug(f'MAIN: filename: {opts[OUT_FILE]}')
cli.parse(file, language, opts)
else:
for file in files:
opts[FILENAME] = default_file_name(file)
cli.parse(file, language, opts)
@beads.command()
@click.argument('search_text', type=str, default=None, required=False)
......@@ -187,6 +195,22 @@ def set_up_logger(verbose: bool) -> None:
logging.debug(f'MAIN: Set up logger with level={log_level}')
def default_file_name(file: IO) -> str:
"""
Get the default name for an input file
:param file: IO user input
:return: default name for file
"""
default_name = file.name.rpartition('.')[0]
if default_name == '':
default_name = f'generated_code_{str(datetime.datetime.now())}'
return default_name
def filter_languages(search_text: str) -> iter:
"""
Filter all supported languages by given SEARCH_TEXT.
......
......@@ -91,6 +91,8 @@ def __find_languages__() -> None:
"""
Find and set all supported languages via the existence of a valid template
"""
from modules.reader import read_config
global supported_languages
global code_templates
global file_associations
......@@ -102,13 +104,7 @@ def __find_languages__() -> None:
first_line = content.readline()
if first_line.startswith('#!'):
config_list: list = first_line.split(' ')
config: dict = {}
for item in config_list:
if ':' in item:
i = item.split(':')
config.update({i[0]: i[1]})
config: dict = read_config(first_line)
if __is_valid_config__(config):
language = config[LANGUAGE]
......
from meta import SKIP_VALIDATION, FILENAME, REPLACE, OUT_DIR, OUT_FILE
from meta import SKIP_VALIDATION, FILENAME, REPLACE, OUT_DIR, NAME
from modules.logic import process
from modules.templates import get_language_association
from modules.templates import append_language_association
from modules.errors import ParsingError, InternalException, ExistingFileError, ValidationError, UnsupportedLanguageError
from modules.clicklog import info, warn, error
from modules import writer, reader
......@@ -25,26 +25,32 @@ def parse(file: IO, language: str, opts: dict) -> None:
fsm: dict = reader.parse_file(file)
logging.debug(f'CLI: Parsed provided file into state machine:\n {fsm}')
skip_validation = opts[SKIP_VALIDATION]
if skip_validation:
warn('Validation of internal logic will be skipped!')
if NAME not in fsm.keys() or fsm[NAME].strip() == "":
fsm[NAME] = 'StateMachine'
code = process(fsm, language, skip_validation)
code = process(fsm, language, opts[SKIP_VALIDATION])
logging.debug(f'CLI: Processed state machine to code:\n{code}')
filename = opts[OUT_FILE] + get_language_association(language)
out_path = os.path.join(opts[OUT_DIR], filename)
filename = append_language_association(opts[FILENAME], language)
if os.path.isabs(filename):
out_path = filename
else:
out_path = os.path.join(opts[OUT_DIR], filename)
try:
logging.debug(f'CLI: Trying to save code in file: {out_path} and replace existing flag: {opts[REPLACE]}')
writer.save_text_file(code, out_path, opts[REPLACE])
logging.debug(f'CLI: Saved code to file')
info(f'Saved generated code in: {out_path}')
except ExistingFileError:
warn(f'File: {out_path} already exists! Code could not be saved!')
warn('Provide "-re" option to automatically overwrite existing files')
info('Execution finished without errors!')
info(f'Parsing of {file.name} finished without errors!')
except ParsingError as pe:
error(f'Content could not be parsed into a valid format!\n{pe.cause}')
......
......@@ -19,11 +19,5 @@ class ParsingError(InternalException):
self.cause = cause
class BrowserError(InternalException):
""" Any error that occurs while trying to find alternative browser """
def __init__(self, cause):
self.cause = cause
class ExistingFileError(InternalException):
""" Error indicating that a file with the same name already exists and the '-re' flag is not set """
......@@ -7,11 +7,6 @@ from distutils.util import strtobool
import logging
import eel
import json
import webbrowser
import platform
import subprocess
import os
import errno
browser_options: dict = {
'window_size': (1200, 800),
......@@ -45,7 +40,12 @@ def start(file: IO, opts: dict) -> None:
if opts[NO_WINDOW]:
startup_options[MODE] = None
eel.start(HTML, options=startup_options, size=browser_options['window_size'])
try:
eel.start(HTML, options=startup_options, size=browser_options['window_size'])
except Exception:
startup_options[MODE] = 'edge'
eel.start(HTML, options=startup_options, size=browser_options['window_size'])
""" GUI API
......@@ -209,28 +209,3 @@ def get_info() -> str:
resource: dict = reader.load_resource('info', 'README.md')
return json.dumps(resource[CONTENT])
def get_browser(browser_path=None) -> webbrowser.BaseBrowser:
browser = webbrowser
if platform.system() == 'Windows':
browser = webbrowser.get('windows-default')
elif platform.system().lower().find('mac'):
browser = webbrowser.get('macosx')
elif browser_path:
browser = webbrowser.get(browser_path)
logging.debug(f'GUI: get_Browser: Browser - {browser}')
logging.debug(f'GUI: get_Browser: Platform - {platform.system()}')
return browser
def is_tool(name):
try:
devnull = open(os.devnull)
subprocess.Popen([name], stdout=devnull, stderr=devnull).communicate()
except OSError as e:
if e.errno == errno.ENOENT:
return False
return True
from meta import RESOURCES_PATH, STANDARD_ENCODING, CONTENT
from meta import *
from modules.errors import ParsingError
from typing import IO
import markdown as md
......@@ -21,12 +21,18 @@ def parse_file(file: IO) -> dict:
if file.name.endswith('.json'):
logging.debug(f'READER: Identified {file.name} as json: Calling json.load')
return json.load(file)
elif file.name.endswith('.bds'):
content = json.load(file)
elif file.name.endswith('.bead'):
logging.debug(f'READER: Identified {file.name} as beads-file: parsing from bds to json')
return parse_bds(file)
content = parse_beads(file)
else:
raise NotImplementedError
raise ParsingError('Unsupported file-type!')
logging.debug(f'READER: Parsed file: {file.name} to :\n{content}')
return content
def parse_text(text: str) -> dict:
......@@ -48,7 +54,7 @@ def parse_text(text: str) -> dict:
raise ParsingError
def parse_bds(file: IO) -> dict:
def parse_beads(file: IO) -> dict:
"""
Parse the provided Beads file into a dictionary.
......@@ -58,93 +64,91 @@ def parse_bds(file: IO) -> dict:
:return: Dictionary containing the string contents
"""
data: Dict[str, List[Dict[str, str]]] = {NODES: [], TRANSITIONS: []}
graph_name: str = 'graph'
nodes: set = set()
initial_node: str or None = None
transitions: list = []
for line in file.readlines():
line = line.decode('UTF-8')
if "#" not in line:
[s1, t, s2] = validate_line(line)
transition = {LABEL: t, FROM: s1, TO: s2}
logging.debug(f'READER: parse_bds: line \n from: {line} to: {transition}\n')
data[TRANSITIONS].append(transition)
first_line = file.readline()
if first_line.strip().startswith('#!'):
config = read_config(first_line)
if START in config.keys():
initial_node = config[START]
if NAME in config.keys():
graph_name = config[NAME]
if initial_node is not None:
nodes.add(initial_node)
for line in strip_comments(file):
[_from, transition, _to] = decode_line(line)
transition = {LABEL: transition, FROM: _from, TO: _to}
for transition in data[TRANSITIONS]:
extract_nodes(transition, data)
transitions.append(transition)
nodes.add(_from)
nodes.add(_to)
return data
content = {
NAME: graph_name,
NODES: wrap_node_names(nodes, initial_node),
TRANSITIONS: transitions
}
return content
def validate_line(transition: str) -> List[str]:
def decode_line(transition: str) -> list:
"""
Validate a line of a Beads file format input
Decode a line of a .bead file
:param transition: str - line to validate
:return: list of names for LABEL, FROM and TO
"""
try:
trans_strings = transition.strip().split(':')
# assert transition string has valid structure
assert len(trans_strings) == 3, 'Invalid transition format\n' \
' expected: state_from:transition:state_to\n' \
' got: ' + transition
assert len(trans_strings) == 3,\
f'Invalid transition format\n expected: state_from:transition:state_to\n got: {transition}'
trans_strings = (trans_strings[0].strip(), trans_strings[1].strip(), trans_strings[2].strip())
# assert names are valid
for string in trans_strings:
for e in string:
assert e.isalnum() or e == '_', f'Invalid sign "{e}" in "{string}":\n' \
' Use only alphanumeric signs (a - Z, 0 - 9) and "_"'
for char in string:
assert char.isalnum() or char == '_',\
f'Invalid sign "{char}" in "{string}":\nUse only alphanumeric signs (a - Z, 0 - 9) and "_"'
except AssertionError as ae:
logging.debug('VALIDATION: Failed due to AssertionError')
raise ValidationError(ae)
logging.debug(f'READER: Decoding of {transition} failed!')
raise ParsingError(ae)
return trans_strings
def extract_nodes(transition: dict, data: dict) -> None:
def wrap_node_names(nodes: set, initial_node: str or None) -> list:
"""
Extract start and destination node from a transition and add it to data.nodes
Wrap all node names into required dicts.
:param transition: dict - to extract nodes from
:param data: dict - to add extracted nodes to
:param initial_node: Optional node defining starting point
:param nodes: Set of node names
"""
for key in [FROM, TO]:
node = handle_start_node_bds(transition[key])
append_node_bds(node, data[NODES])
nodes_list: list = []
for node_name in nodes:
node = {ID: node_name}
def handle_start_node_bds(name: str) -> dict:
"""
Append {START: True} to node if name starts with _
if initial_node and node_name == initial_node:
node[START] = True
Method to parse input file into an internally usable version (dict).
:param name: str - name of node to check for start condition
:return: node as Dictionary
"""
nodes_list.append(node)
node: dict = {ID: name}
if not name.startswith('_'):
logging.debug(f'READER: handle_start_node: RETURNS NODE: {node}\n')
else:
node[START] = True
logging.debug(f'READER: handle_start_node: RETURNS START-NODE: {node}\n')
return node
def append_node_bds(node: dict, nodes: list) -> None:
"""
Append node to nodes if it does not exist yet
:param node: dict to add to nodes
:param nodes: list to add node to
"""
if node not in nodes:
nodes.append(node)
logging.debug(f'READER: parse_bds: Node added {node}\n')
return nodes_list
def load_resource(subdir: str, name: str) -> dict:
......@@ -157,6 +161,7 @@ def load_resource(subdir: str, name: str) -> dict:
:param name: Filename of the resource
:return: A parsed dictionary of the specified resource
"""
resource_path = os.path.join(RESOURCES_PATH, subdir, name)
logging.debug(f'READER: Constructed resource_path: {resource_path}')
......@@ -181,6 +186,29 @@ def load_resource(subdir: str, name: str) -> dict:
return res
def read_config(line: str) -> dict:
"""
Read the config line of a file.
Config lines start with #! and declare configuration item like name:value.
Items need to be separated by whitespace.
:param line: Config line as str
:return: parsed config as dict
"""
config: dict = {}
config_list: list = line.split(' ')
for element in config_list:
if ':' in element:
item = element.split(':')
config.update({item[0]: item[1].replace('\n', '')})
return config
def strip_comments(file: IO) -> str:
"""
Clear a resource file of comments.
......@@ -202,4 +230,6 @@ def strip_comments(file: IO) -> str:
"""
for line in file.readlines():
yield line.partition('#')[0]
line = line.partition('#')[0].rstrip()
if line:
yield line
......@@ -27,10 +27,11 @@ def get_template(language: str) -> dict:
return template
def get_language_association(language: str) -> str:
def append_language_association(file_name: str, language: str) -> str:
"""
Get the file_type for the provided language.
Append the file_type for the provided language if necessary.
:param file_name: Filename to append association on.
:param language: string for programming language
:raises UnsupportedLanguageError if language is not supported
:return: File_type for provided language
......@@ -39,7 +40,12 @@ def get_language_association(language: str) -> str:
if is_unsupported_language(language):
raise UnsupportedLanguageError
return file_associations[language]
file_association = file_associations[language]
if not file_name.endswith(file_association):
file_name = file_name + file_association
return file_name
def is_unsupported_language(language: str) -> bool:
......
......@@ -11,6 +11,7 @@ def inspect(fsm: dict) -> None:
Provide a dictionary that represents a fine state machine as:
{
name: "",
nodes: [
{id: , ...}
],
......@@ -29,6 +30,7 @@ def inspect(fsm: dict) -> None:
# assert fsm contains necessary keys
keys = fsm.keys()
assert NAME in keys, f'Missing required key: {NAME}'
assert NODES in keys, f'Missing required key: {NODES}!'
assert TRANSITIONS in keys, f'Missing required key: {TRANSITIONS}!'
......
......@@ -32,19 +32,19 @@ def save_text_file(text: str, path: str, overwrite_existing: bool = False) -> No
def save_resource(subdir: str, name: str, full_content: dict) -> None:
"""
Write the provided FULL_CONTENT into a file with given NAME.
"""
Write the provided FULL_CONTENT into a file with given NAME.
:param subdir: Sub directory of the resources folder
:param name: Filename
:param full_content: Full resources content as dict
"""
:param subdir: Sub directory of the resources folder
:param name: Filename
:param full_content: Full resources content as dict
"""
resource_path: str = os.path.join(RESOURCES_PATH, subdir, name)
resource_path: str = os.path.join(RESOURCES_PATH, subdir, name)
content: str = json.dumps(full_content)
content: str = json.dumps(full_content)
save_text_file(content, resource_path, True)
save_text_file(content, resource_path, True)
def overwrite(text: str, path: str) -> None:
......@@ -56,12 +56,15 @@ def overwrite(text: str, path: str) -> None:
:param path: PATH to save into
:raises IOError
"""
try:
output_file = open(path, 'w+')
output_file.write(text)
output_file.close()
logging.debug(f'WRITER: Writing successful for file: {path}')
out_dir: str = os.path.dirname(path)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
output_file = open(path, 'w+')
output_file.write(text)
output_file.close()
logging.debug(f'WRITER: Writing successful for file: {path}')
except IOError as ioe:
raise ioe
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment