diff --git a/pyproject.toml b/pyproject.toml index 49690ac..59503ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,14 +3,14 @@ requires = ["setuptools>=64", "wheel", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [tool.setuptools] -py-modules = ["wdl"] +packages = ["wdl"] [tool.setuptools_scm] version_file = "wdl/_version.py" [project] name = "wdl" -dyanmic = ["version"] +dynamic = ["version"] description = "WDL (Waveform Definition Language) is designed for use with Archons. This library provides tools and instructions for setting up and using WDL efficiently." authors = [{name = "Caltech Optical Observatories"}] @@ -20,6 +20,18 @@ dependencies = [ "matplotlib>=3.9.2", "PyQt5>=5.15" ] + +requires-python = ">=3.12" + [project.urls] Repository = "https://github.com/CaltechOpticalObservatories/wdl" + +[project.scripts] +wdl = "wdl.cli:main" +ini2acf = "wdl.ini2acf:main" + +# Legacy command wrappers for historical *.py driver scripts +"LexerDriver.py" = "wdl.LexerDriver:__main__" + + diff --git a/wdl/Lexer.py b/wdl/Lexer.py index 6e075fc..aa25107 100644 --- a/wdl/Lexer.py +++ b/wdl/Lexer.py @@ -29,10 +29,21 @@ # David Hale or # Stephen Kaye -import genericScanner as Scanner -from genericToken import * -from Symbols import * -from genericCharacter import * + +if __package__ != "wdl": + import os + import warnings + basen = os.path.basename(__file__) + warnings.warn(f"detected running a script directly, consider using python -m wdl.{basen}") + import genericScanner as Scanner + from genericToken import * + from Symbols import * + from genericCharacter import * +else: + from . import genericScanner as Scanner + from .genericToken import * + from .Symbols import * + from .genericCharacter import * character = "" c1 = "" diff --git a/wdl/LexerDriver.py b/wdl/LexerDriver.py index 974148c..c8c613d 100755 --- a/wdl/LexerDriver.py +++ b/wdl/LexerDriver.py @@ -22,8 +22,8 @@ # David Hale or # Stephen Kaye -import Lexer -from Symbols import EOF +from . import Lexer +from .Symbols import EOF import fileinput diff --git a/wdl/Symbols.py b/wdl/Symbols.py index a46e6da..d17ae75 100644 --- a/wdl/Symbols.py +++ b/wdl/Symbols.py @@ -108,7 +108,7 @@ IDENTIFIER_STARTCHARS = string.ascii_letters IDENTIFIER_CHARS = string.ascii_letters + string.digits + "_" -NUMBER_STARTCHARS = string.digits +NUMBER_STARTCHARS = string.digits + '-' NUMBER_CHARS = string.digits + "." STRING_STARTCHARS = "'" + '"' diff --git a/wdl/__main__.py b/wdl/__main__.py new file mode 100644 index 0000000..8b0a4b4 --- /dev/null +++ b/wdl/__main__.py @@ -0,0 +1,4 @@ +from wdl.cli import main + +if __name__ == "__main__": + main() diff --git a/wdl/cli.py b/wdl/cli.py new file mode 100644 index 0000000..a6f97ce --- /dev/null +++ b/wdl/cli.py @@ -0,0 +1,58 @@ +from argparse import ArgumentParser +import sys +from .commands.driverbase import WDLDriver +from .commands.legacy_drivers import ( + SeqParserDriver, ModParserDriver, IncParserDriver, + WdlParserDriver, WavgenDriver, ModegenDriver, Ini2acfDriver) + +from .commands.preprocess import FindGPP, PreprocessGPP + +import logging + +logger = logging.getLogger(__name__) + +def main(): + ap = ArgumentParser(prog="wdl", + description="command line interface to Waveform Definition Language (WDL)") + ap.add_argument("--debug", help="print verbose debugging output", action="store_true") + subparsers = ap.add_subparsers(required=True, help="the WDL subcommand to run", + dest="command_name") + + #NOTE: could have done a fancy autoreg thing here but there's only a few and this is likely + #clearer until there are lots more IMO + command_classes: list[type] = [SeqParserDriver, ModParserDriver, IncParserDriver, WdlParserDriver, + WavgenDriver, ModegenDriver, Ini2acfDriver, FindGPP, PreprocessGPP] + + for cls in command_classes: + cls.setup_subparser(subparsers) + + args, unknown_args = ap.parse_known_args() + + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + #the default func is setup by the class method above to just return which class it is + #that will be operating the command + run_command_tp: type[WDLDriver] = args.func(args) + + if run_command_tp.ACCEPTS_EXTRA_ARGS: + obj = run_command_tp(unknown_args, **vars(args)) + elif len(unknown_args) > 0: + logger.error("extra (unknown) arguments provided to command that doesn't accept them") + logger.error(f"those arguments were: {unknown_args}") + #my instinct is to throw here but that seems unpopular round these parts so just bail out + sys.exit(1) + else: + kwargs = vars(args) + obj = run_command_tp(**kwargs) + + #run the command + return obj(cli_mode=True) + + +if __name__ == "__main__": + retcode = main() + sys.exit(retcode) diff --git a/wdl/commands/__init__.py b/wdl/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wdl/commands/driverbase.py b/wdl/commands/driverbase.py new file mode 100644 index 0000000..38835fb --- /dev/null +++ b/wdl/commands/driverbase.py @@ -0,0 +1,70 @@ +from abc import ABCMeta, abstractmethod +from argparse import ArgumentParser, _SubParsersAction, Namespace +from pathlib import Path +from io import TextIOWrapper +from sys import stdin +from contextlib import contextmanager +from typing import ContextManager + +class WDLDriver(metaclass=ABCMeta): + ACCEPTS_EXTRA_ARGS: bool = False + + @classmethod + def setup_subparser(cls, subparsers: _SubParsersAction, fname_arg_setup: bool=True) -> ArgumentParser: + """sets up a sub-parser for arguments specific to this particular type of WDL subcommand driver""" + + #Get a name to put in the parser, either explicit or named after the class + cmdname: str = getattr(cls, "CMD_NAME", cls.__name__) + cmddesc: str = getattr(cls, "CMD_DESCRIPTION", None) + parser = subparsers.add_parser(name=cmdname, description=cmddesc) + + if fname_arg_setup: + parser.add_argument("fname", help="file to read input from. Use '-' to read from stdin") + + def cls_provider(args: Namespace) -> None: + return cls + + parser.set_defaults(func=cls_provider) + return parser + + @classmethod + def find_input_files(cls, basepath: str | Path) -> list[str]: + if not hasattr(cls, "CMD_FILE_EXTENSIONS"): + raise AttributeError("don't know file extensions to search for") + if isinstance(basepath, str): + basepath: Path = Path(basepath) + for extn in cls.CMD_FILE_EXTENSIONS: + possible_files = basepath.glob(f"*.{extn}") + return possible_files + + def __init__(self, fname: str, **kwargs): + """The default __init__ of a driver takes a filename and reads it into the ._text attribute of the class. + Since this is the most common operation of a WDL driver program""" + self._text = self._read_file_or_stdin(fname) + + @abstractmethod + def __call__(self, cli_mode: bool) -> None: + """This method will be called when the driver runs from the command line interface""" + pass + + @contextmanager + def _file_or_stdin(self, fname: str) -> ContextManager[TextIOWrapper]: + """context manager that opens a specified file or uses sys.stdin for the special case of file '-'""" + if fname == "-": + yield stdin + else: + f = open(fname, "r") + yield f + f.close() + + @contextmanager + def _file_or_stdout(self, output: TextIOWrapper | str) -> ContextManager[TextIOWrapper]: + if isinstance(output, str): + f = open(output, "w") + yield f + else: + yield output + + def _read_file_or_stdin(self, fname: str) -> str: + with self._file_or_stdin(fname) as f: + return f.read() diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py new file mode 100644 index 0000000..6c870cd --- /dev/null +++ b/wdl/commands/legacy_drivers.py @@ -0,0 +1,171 @@ +import logging +import warnings +from typing import Optional +from argparse import ArgumentParser +from sys import stdout +import os +from .driverbase import WDLDriver + +from wdl.ini2acf import generate_acf + + +# horrible legacy code imports here, TODO: gradually chip away at it +import wdl.wavgen as wavgen +import wdl.modegen as modegen +from wdl.wdlParser import make_include_sequence, parse_modules, parse_system, make_include +import wdl.wdlParser as Parser + +# TODO: separate out plotting, no need for this +import matplotlib.pyplot as plt + + +logger = logging.getLogger(__name__) + + +class SeqParserDriver(WDLDriver): + CMD_NAME: str = "seq" + CMD_DESCRIPTION: str = "parse a .conf file and make an include list from it" + + def __call__(self, cli_mode: bool) -> int: + logger.info("making include sequence") + make_include_sequence(self._text) + return 0 + + +class ModParserDriver(WDLDriver): + CMD_NAME: str = "mod" + CMD_DESCRIPTION: str = "parse a .mod file and create .system and .modules from it" + + @classmethod + def setup_subparser(cls, *args, **kwargs) -> ArgumentParser: + parser = super().setup_subparser(*args, **kwargs) + parser.add_argument("--projname", type=str, help="""project name to use. If not given, + the legacy behaviour is followed (the first line of the input text is assumed to be the project name""") + return parser + + def __init__(self, fname: str, projname: Optional[str] = None, **kwargs): + with self._file_or_stdin(fname) as f: + if projname is None: + warnings.warn("parsing module file assuming the first line is the project name... please use the --name argument to the command instead") + projname = f.readline().strip() + + logger.info("project name is: %s", projname) + self._projname: str = projname + self._text: str = f.read() + + def __call__(self, cli_mode: bool) -> int: + logger.debug("writing output to .modules file...") + self._write_output("CONFIG", "modules", parse_modules(self._text)) + + #Presumably the reason parse_system() here needs no keywords is some hideous global state + # awfulness. DO NOT MOVE THIS LINE ABOVE THE PREVIOUS ONE, therefore + logger.debug("writing output to .system file...") + self._write_output("SYSTEM", "system", parse_system()) + return 0 + + def _write_output(self, archonkw: str, fileext: str, output: str) -> int: + fname: str = f"{self._projname}.{fileext}" + logger.debug("filename is: %s", fname) + with open(fname, "w") as f: + f.writelines([f"[{archonkw}]{os.linesep}"]) + f.write(output) + +class IncParserDriver(WDLDriver): + CMD_NAME: str = "inc" + CMD_DESCRIPTION: str = "parse an include file" + + def __call__(self, cli_mode: bool) -> int: + make_include(self._text) + return 0 + +class WdlParserDriver(WDLDriver): + CMD_NAME: str = "wdl" + CMD_DESCRIPTION: str = "parse the subroutines from a WDL input file" + + def __call__(self, cli_mode: bool) -> int: + logger.info("parsing WDL file...") + + # TODO:blergh + global subroutines + subroutines = Parser.get_subroutines(self._text) + Parser.get_params(self._text) + output: str = Parser.parse(self._text) + + #apparently this one just prints it out, to stdout I guess? + stdout.write(output) + return 0 + +class WavgenDriver(WDLDriver): + CMD_NAME: str = "wavgen" + CMD_DESCRIPTION: str = "generate archon waveforms from WDL description" + + @classmethod + def setup_subparser(cls, *args, **kwargs) -> ArgumentParser: + parser = super().setup_subparser(*args, **kwargs) + parser.add_argument("--plots", action="store_true", help="generate plots to go with waveforms") + return parser + + def __init__(self, fname: str, plots: bool, **kwargs): + #For some reason I don't understand, legacy wavegenDriver.py wants the BASE NAME of the project as + #the argument, not the actual filename. Implement this behaviour here for compatibility even though + #I must stress it seems a little odd + + self._fname: str = fname + self._plots: bool = plots + + def __call__(self, cli_mode: bool) -> int: + # global variable because OF COURSE IT IS + wavgen.GenerateFigs = self._plots + wavgen.loadWDL(f"{self._fname}.wdl", self._fname) + + # ok this breaks previous behaviour because even if plot option was false + # the original program still did a plt.show(). This seems utterly without reason to me + # so here we only actually do that if it was asked for... + # Again, all the plots are done with implicit global state rather than using the MPL object interface... + if self._plots: + plt.show(block=True) + + return 0 + + +class ModegenDriver(WDLDriver): + CMD_NAME: str = "modegen" + CMD_DESCRIPTION: str = "generate archon modes from WDL description" + + @classmethod + def setup_subparser(cls, subparsers) -> ArgumentParser: + parser = super().setup_subparser(subparsers, fname_arg_setup=False) + #need two filenames here, a mode file and an acf file + parser.add_argument("modefile", type=str, help="the mode file to use") + parser.add_argument("acffile", type=str, help="the ACF file to append to") + return parser + + def __init__(self, modefile: str, acffile: str, **kwargs): + self._modefile = modefile + self._acffile = acffile + + def __call__(self, cli_mode: bool, append: bool=True) -> int: + mobj = modegen.Modegen(self._modefile, self._acffile) + mobj.write(append) + return 0 + +class Ini2acfDriver(WDLDriver): + CMD_NAME: str = "ini2acf" + CMD_DESCRIPTION: str = "convert a INI syntax file into ACF" + + @classmethod + def setup_subparser(cls, subparsers) -> ArgumentParser: + parser = super().setup_subparser(subparsers) + parser.add_argument("-o,--outfile",help="output file to use. By default, outputs to stdout", type=str) + return parser + + def __init__(self, fname: str, outfile: Optional[str]=None, **kwargs): + super().__init__(fname) + self._outfile = stdout if outfile is None else outfile + + def __call__(self, cli_mode: bool) -> int: + outtxt: str = generate_acf(self._text, treat_str_as_content=True) + with self._file_or_stdout(self._outfile) as f: + logger.debug(f"outtxt is: {outtxt}") + f.write(outtxt) + return 0 diff --git a/wdl/commands/preprocess.py b/wdl/commands/preprocess.py new file mode 100644 index 0000000..74ef78b --- /dev/null +++ b/wdl/commands/preprocess.py @@ -0,0 +1,61 @@ +from .driverbase import WDLDriver +from argparse import ArgumentParser +from sys import stdout +from shutil import which +import os +import warnings +from typing import Optional +import logging +import subprocess + +logger = logging.getLogger(__name__) + +#TODO: something cleverer on windows, if that +#ever happens +def _find_gpp_program() -> Optional[str]: + return which("gpp") + + +class FindGPP(WDLDriver): + CMD_NAME: str = "find_gpp" + + @classmethod + def setup_subparser(cls, subparsers) -> ArgumentParser: + return super().setup_subparser(subparsers, fname_arg_setup=False) + + def __init__(self, **kwargs): + #For now, just find system gpp, in future we can check bundled as well + self._gpppath = _find_gpp_program() + + def __call__(self) -> int: + if self._gpppath is not None: + stdout.write(f"{self._gpppath}{os.linesep}") + return 0 + else: + warn("did not find GPP...") + stdout.write(f"{os.linesep}") + return 1 + +class PreprocessGPP(WDLDriver): + CMD_NAME: str = "gpp_preprocess" + ACCEPTS_EXTRA_ARGS: bool = True + + @classmethod + def setup_subparser(cls, subparsers) -> ArgumentParser: + return super().setup_subparser(subparsers, fname_arg_setup=False) + + def __init__(self, gppargs: list[str], **kwargs): + self._gpppath = _find_gpp_program() + self._gppargs = gppargs + print(f" keyword args: {kwargs}") + + def __call__(self, cli_mode: bool) -> int: + if self._gpppath is None: + stdout.write("could not find GPP program, the gpp_preprocess command cannot continue") + return 1 + + logger.debug("gpp path is: %s", self._gpppath) + logger.debug("extra gpp args are: %s", str(self._gppargs)) + + procresult = subprocess.run(self._gpppath, self._gppargs) + return procresult.return_code diff --git a/wdl/genericScanner.py b/wdl/genericScanner.py index 050d52e..7211aa7 100644 --- a/wdl/genericScanner.py +++ b/wdl/genericScanner.py @@ -27,7 +27,17 @@ # David Hale or # Stephen Kaye -from genericCharacter import * + +#hack to allow both modern style import and direct script execution +if __package__ != "wdl": + import os + import warnings + basen = os.path.splitext(os.path.basename(__file__))[0] + warnings.warn(f"detected running a script directly, consider using python -m wdl.{basen}") + import genericScanner as Scanner + from genericCharacter import * +else: + from .genericCharacter import * """ A Scanner object reads through the source_text diff --git a/wdl/incParserDriver.py b/wdl/incParserDriver.py index 6d283c4..1208993 100755 --- a/wdl/incParserDriver.py +++ b/wdl/incParserDriver.py @@ -22,9 +22,17 @@ # David Hale or # Stephen Kaye +#hack to allow both modern style import and direct script execution +if __package__ != "wdl": + import os + import warnings + basen = os.path.basename(__file__) + warnings.warn(f"detected running a script directly, consider using python -m wdl.{basen}") + import wdlParser as Parser +else: + from . import wdlParser as Parser import fileinput -import wdlParser as Parser import sys sys.dont_write_bytecode = True diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py new file mode 100644 index 0000000..b872eea --- /dev/null +++ b/wdl/ini2acf.py @@ -0,0 +1,127 @@ +"""Rewrite of ini2acf.pl utility in reasonably modern and intelligible python""" + +from io import TextIOWrapper +from pathlib import Path +from typing import Generator, Optional +import sys +from argparse import ArgumentParser +from os import linesep + +def _extract_section_name(line: str) -> Optional[tuple[str, bool]]: + if '[' not in line: + return None + else: + secname: str = line.split("[")[1].split("]")[0] + if '#' not in secname: + return secname, False + return secname.strip('#'), True + + +def _section_replace_filter(inp: str) -> Generator[tuple[str, list[str]], None, None]: + #The original script uses a regex here to get the tag names but we'll just do it a + #way that even a physicist can understand here + thissection: Optional[str] = None + thisprocess: Optional[bool] = None + seclines: list[str] = [] + for line in inp.splitlines(): + if (tpl :=_extract_section_name(line)) is not None: + secname, process = tpl + # This is a new section. If we are already processing one, yield it out and start the next + # Otherwise, start processing the lines + if thissection is not None: + yield thissection, seclines, thisprocess + thissection, thisprocess = secname, process + seclines.clear() + else: + #this is a continuation of the previous section, just append the lines + seclines.append(line) + yield thissection, seclines, thisprocess + +def generate_acf(inifile: str | Path | TextIOWrapper, + treat_str_as_content: bool=False) -> str: + """the way this seems to work is the WDL legacy tools spit out files that have an ini section that is labelled e.g. + [PARAMETER#]... with the # character in it. Here, we are (I think) supposed to just go through the items in that section + and number them""" + + + #note my first thought here was to use the built in INI parsing "configparser" module, + #but unfortunately, it can't quite handle things like the [PARAMETER#] lines. Shame + + if isinstance(inifile, Path) or (isinstance(inifile, str) and not treat_str_as_content): + with open(inifile, "r") as f: + inp: str = f.read() + elif isinstance(inifile, str): + #This is a string containing the content of an inifile + inp = inifile + else: + #This is an already open file like opject + inp: str = inifile.read() + outp: list[str] = [] + + #need to add [CONFIG] at the top + outp.append(f"[CONFIG]{linesep}") + + for secname, seclines, process in _section_replace_filter(inp): + if process: + for ind, line in enumerate(seclines): + #strip whitespace and remove any trailing comment + line = line.split("#")[0].strip() + newline: str = f'{secname}{ind}="{line}"' + outp.append(newline) + # A line telling us how many we had + outp.append(f'{secname}S={ind+1}') + else: + outp.append(f"[{secname}]") + outp.extend(seclines) + return f"{linesep.join(outp)}{linesep}" + +def main(): + ap = ArgumentParser(prog="ini2acf", + description=""" + + This is a python rewrite of David Hale and Peter Mao's ini2acf.pl script. + It is intended to be call compatible with that script. The original + documentation follows... + + Format an INI type file for insertion into ACF. Removes comments, and + trailing whitespace. + + In the input file, lines of the same tag type are grouped under [TAG]. + When the tag is specified as [TAG#], the acf lines are numbered and + the number of lines of a tag type are reported when a new tag is + encountered or the end of file is reached. + + Appropriate for numbered tags: LINE, PARAMETER, CONSTANT, TAPLINE + + This code is NOT smart enough to know if you use the same tag in + two disjoint parts of the INI file. + + """) + ap.add_argument("infile", help="input ini file", type=str) + ap.add_argument( + "-o", + "--outfile", + help="output file to use. By default, outputs to stdout", + type=str, + ) + + args = ap.parse_args() + + out_content: str = generate_acf(args.infile) + + if args.outfile is not None: + with open(args.outfile,"w") as f: + f.write(out_content) + + else: + sys.stdout.write(out_content) + + return 0 + + +if __name__ == "__main__": + #basic direct call behaviour to mimic the old ini2acf.pl script + # TODO + retcode = main() + sys.exit(retcode) + diff --git a/wdl/modParserDriver.py b/wdl/modParserDriver.py index eee328b..289ee43 100755 --- a/wdl/modParserDriver.py +++ b/wdl/modParserDriver.py @@ -23,13 +23,30 @@ # Stephen Kaye + import fileinput -import wdlParser as Parser import sys -sys.dont_write_bytecode = True -sys.tracebacklimit = 0 +#hack to allow both modern style import and direct script execution +if __package__ in (None, ""): + import sys + import warnings + from pathlib import Path + + repo_root = Path(__file__).resolve().parent.parent + sys.path.insert(0, str(repo_root)) + + module_name = Path(__file__).stem + warnings.warn( + f"Detected direct script execution. " + f"Consider using: python -m wdl.{module_name}", + RuntimeWarning, + stacklevel=2, + ) + from wdl import wdlParser as Parser +else: + from . import wdlParser as Parser # ----------------------------------------------------------------------------- # @fn main diff --git a/wdl/modegen.py b/wdl/modegen.py index ba8629a..5418fba 100644 --- a/wdl/modegen.py +++ b/wdl/modegen.py @@ -38,6 +38,7 @@ def __init__(self, modefile, acffile): # print error messages for keys in union that are not # declared the default mode + print(f"union: {self.union}") for key in self.union: if self.union[key] is None: print("WARNING: '%s' needs to be defined in MODE_DEFAULT." % key) @@ -48,7 +49,7 @@ def __read_inputfile(self): with open(self.modefile) as FILE: for line in FILE: # look for headers - match = re.search("^\[(.*?)\]", line) + match = re.search(r"^\[(.*?)\]", line) if match: # initialize the KVpair entry self.modeKVpair.update({match.group(1): {}}) @@ -57,8 +58,9 @@ def __read_inputfile(self): thismode = match.group(1) else: # look for key=value pairs, matching the LAST = on the line - match = re.search("^(.+:.+)\s*=\s*(.+?)\n", line) + match = re.search(r"^(.+:.+)\s*=\s*(.+?)\n", line) if match: + print(f"found match: {match} group 1 {match.group(1)}") # union will hold one of every key specified self.union.update({match.group(1): None}) # modes only hold specified key=value pairs @@ -67,8 +69,10 @@ def __read_inputfile(self): ) # if the key is a non-empty TAPLINE setting, # then increment self.taplines[thismode] - if re.search('ACF:TAPLINE\d+="[\w,]+"', line): + if re.search(r'ACF:TAPLINE\d+=\"[\w,-]+\"', line): + print(f"found tapline {match.group(2)}") if thismode not in self.taplines: + print("updating tapline!") self.taplines.update({thismode: 1}) else: self.taplines[thismode] += 1 @@ -79,7 +83,7 @@ def __read_inputfile(self): self.modelist[thismode].append(match.group(1)) continue # both matches failed - issue warning - if re.search("[^\w]+", line[:-1]): + if re.search(r"[^\w]+", line[:-1]): print( "WARNING: '%s' in %s not recognized as a " "mode-setting statement" % (line[:-1], thismode) @@ -98,7 +102,7 @@ def __assign_defaults_from_acf(self): with open(self.acffile) as ACF: for line in ACF: # skip [MODE_X] statments - if re.search("^\w+:\w", line): + if re.search(r"^\w+:\w", line): continue # look for key=value pairs in ACF match = re.search("^(.+)=(.+?)\n", line) @@ -118,7 +122,7 @@ def __assign_defaults_from_acf(self): if re.search("%d", unionkey): # make regex from printf %d ONLY IF # it appears in the key - unionregex = re.sub("%d", "(\d+)", unionkey) + unionregex = re.sub("%d", r"(\d+)", unionkey) kmatch = re.search(unionregex, ACFKEY) if kmatch: try: @@ -151,7 +155,7 @@ def __assign_defaults_from_acf(self): print("**NOTE** 'ACF:'-type keys use default values from %s" % self.acffile) def __index_modeKVpair(self): - """convert (\d+)'s into proper ACF indices""" + """convert (\\d+)'s into proper ACF indices""" for mode in self.modeKVpair: for key in self.modeKVpair[mode]: # search the "ACF:" keys for "%d=" @@ -161,7 +165,7 @@ def __index_modeKVpair(self): # or MOD ACF statements, as they would not be unique if match: # now figure out which key in self.union this corresponds to - keyregex = re.sub("%d", "(\d+)", key) + keyregex = re.sub("%d", r"(\d+)", key) for ukey in self.union: kmatch = re.search(keyregex, ukey) if kmatch: diff --git a/wdl/modegenDriver.py b/wdl/modegenDriver.py index 9c7db9c..e75796b 100755 --- a/wdl/modegenDriver.py +++ b/wdl/modegenDriver.py @@ -32,9 +32,32 @@ # Stephen Kaye # import fileinput + +# Hack to allow both modern style import and direct script execution +if __package__ in (None, ""): + import sys + import warnings + from pathlib import Path + + repo_root = Path(__file__).resolve().parent.parent + sys.path.insert(0, str(repo_root)) + + module_name = Path(__file__).stem + warnings.warn( + f"Detected direct script execution. " + f"Consider using: python -m wdl.{module_name}", + RuntimeWarning, + stacklevel=2, + ) + + from wdl import wavgen + from wdl import modegen +else: + from . import wavgen + from . import modegen + + import matplotlib.pyplot as plt -import wavgen -import modegen import sys sys.dont_write_bytecode = True diff --git a/wdl/seqParserDriver.py b/wdl/seqParserDriver.py index e4209f7..700b327 100755 --- a/wdl/seqParserDriver.py +++ b/wdl/seqParserDriver.py @@ -22,8 +22,30 @@ # David Hale or # Stephen Kaye +#hack to allow both modern style import and direct script execution +if __package__ in (None, ""): + import sys + import warnings + from pathlib import Path + + # Add the repo/package parent directory so `import wdl...` works + # even when this file is run directly. + repo_root = Path(__file__).resolve().parent.parent + sys.path.insert(0, str(repo_root)) + + module_name = Path(__file__).stem + warnings.warn( + f"Detected direct script execution. " + f"Consider using: python -m wdl.{module_name}", + RuntimeWarning, + stacklevel=2, + ) + + from wdl import wdlParser as Parser +else: + from . import wdlParser as Parser + import fileinput -import wdlParser as Parser import sys sys.dont_write_bytecode = True diff --git a/wdl/wavgen.py b/wdl/wavgen.py index e1e58cf..62cc359 100644 --- a/wdl/wavgen.py +++ b/wdl/wavgen.py @@ -92,7 +92,7 @@ def loadWDL(infile, outfile="/dev/null", verbose=1): with open(infile, "r") as f: for line in f: # look for mod file - match = re.search("^modulefile\s+([~\w./]+)\s*$", line) + match = re.search(r"^modulefile\s+([~\w./]+)\s*$", line) if match is not None: ModFile = os.path.abspath(os.path.expanduser(match.group(1))) wdl_file_did_not_specify_mod_file = False @@ -116,28 +116,28 @@ def loadWDL(infile, outfile="/dev/null", verbose=1): if line == "": continue # look for mod file - match = re.search("^modulefile\s+([~\w./]+)\s*$", line) + match = re.search(r"^modulefile\s+([~\w./]+)\s*$", line) if match is not None: continue # look for signal file - match = re.search("^signalfile\s+([~\w./]+)\s*$", line) + match = re.search(r"^signalfile\s+([~\w./]+)\s*$", line) if match is not None: __SignalFile__ = os.path.abspath(os.path.expanduser(match.group(1))) if not os.path.isfile(__SignalFile__): print("Signal file specified does not exist (%s)" % __SignalFile__) continue # look for parameters - match = re.search("^parameter\s+(\w+)=(\d+)\s*$", line) + match = re.search(r"^parameter\s+(\w+)=(\d+)\s*$", line) if match is not None: pname = match.group(1) pval = int(match.group(2)) Parameters.update({pname: pval}) continue # look for constants - match = re.search("^constant\s+(\w+)=(\d+)\s*$", line) + match = re.search(r"^constant\s+(\w+)\s*=\s*([+-]?\d+(?:\.\d*)?)\s*$", line) if match is not None: cname = match.group(1) - cval = int(match.group(2)) + cval = float(match.group(2)) Constants.update({cname: cval}) continue # look for a label @@ -165,7 +165,7 @@ def loadWDL(infile, outfile="/dev/null", verbose=1): thisTS.sequenceDef.append([ctr, line[:-1]]) ctr += 1 elif TStype == "waveform": - match = re.search("(\d+)\s+(\d+)\s+(\d+)\s+([+-]?[\d\.]+)", line) + match = re.search(r"(\d+)\s+(\d+)\s+(\d+)\s+([+-]?[\d\.]+)", line) if match is not None: # body of a waveform time = int(match.group(1)) @@ -204,7 +204,7 @@ def loadWDL(infile, outfile="/dev/null", verbose=1): ) else: # handle the end line of a waveform. - match = re.search("(\d+)\s+(\w+)", line) + match = re.search(r"(\d+)\s+(\w+)", line) thisTS.nperiods = int(match.group(1)) if thisTS.nperiods == 0: print( @@ -264,7 +264,7 @@ def __loadMod__(ModFile): # subroutine of loadWDL() # btype = [] with open(ModFile, "r") as f: for line in f: - match = re.search("^\s*SLOT\s+(\d+)\s+(\w+)\s{", line) + match = re.search(r"^\s*SLOT\s+(\d+)\s+(\w+)\s{", line) if match is not None: thisSlotNum = int(match.group(1)) thisBoardLabel = match.group(2) @@ -341,7 +341,7 @@ def __loadSignals__(__SignalFile__): # subroutine of loadWDL() with open(__SignalFile__, "r") as f: for line in f: # look for signal file - match = re.search("^#define (\w+)\s+(\d+)\s+:\s+(\d+)", line) + match = re.search(r"^#define (\w+)\s+(\d+)\s+:\s+(\d+)", line) if match is not None: signame = match.group(1) sigslot = int(match.group(2)) @@ -757,7 +757,7 @@ def script(self, outfile=sys.stdout): EOL = True if this_sub_call[0:3].upper() == "IF ": # get the first word after IF... - regexmatch = re.search("\w+\s+!?([\w]+)", this_sub_call) + regexmatch = re.search(r"\w+\s+!?([\w]+)", this_sub_call) this_param = regexmatch.group(1) try: # demo if this_param is an integer int(this_param) @@ -901,9 +901,9 @@ def __make_waveform(self, initialLevel=None): if len(seq_indx): this_sub_call = self.sequenceDef[seq_indx[0]][1] match = re.search( - "(IF\s+(?P!)?(?P\w+)(?P--)?\s+)?" - + "((?PRETURN|GOTO|CALL)\s+(?P\w+)\(?)?" - + "(\(?(?P\w+)?(?P--)?)\)?", + r"(IF\s+(?P!)?(?P\w+)(?P--)?\s+)?" + + r"((?PRETURN|GOTO|CALL)\s+(?P\w+)\(?)?" + + r"(\(?(?P\w+)?(?P--)?)\)?", this_sub_call, ) # REGEX labels: @@ -1037,7 +1037,7 @@ def plot(self, cycles=2, initialLevel=None): axes[kk].grid("on") if kk > 0: axes[kk].set_xticklabels([]) - axes[0].set_xlabel("time [$\mu$s]") + axes[0].set_xlabel(r"time [$\mu$s]") axes[kk].set_title("non-static waveforms for %s" % self.name) plt.draw() print("(Figure %d)" % self.label, end=" ") @@ -1268,7 +1268,7 @@ def state(outfile=None): statestring += "0,1,0" elif (KeepSum + 1) == __chan_per_board__["hvbd"]: # proper change # 2. get the level corresponding to the non-keep. - hvbd_chan = np.where(hvbdKeep == 0)[0] + hvbd_chan = int(np.where(hvbdKeep == 0)[0][0]) statestring += "1,%d,%g" % (hvbd_chan + 1, hvbdLevel[hvbd_chan]) else: print("Error in HVBD state call -- multiple changes in a state") @@ -1403,7 +1403,7 @@ def script(outfile=None, quiet=False): if len(Constants) > 0: outfilehandle.write("[CONSTANT#]\n") for const in Constants: - outfilehandle.write("%s=%d\n" % (const, Constants[const])) + outfilehandle.write("%s=%f\n" % (const, Constants[const])) outfilehandle.write("[LINE#]\n") if outfilehandle.name != "": outfilehandle.close() @@ -1449,4 +1449,4 @@ def wplot(TimingObjectLabel): Catalog[__index_of__(TimingObjectLabel)].plot() - return \ No newline at end of file + return diff --git a/wdl/wavgenDriver.py b/wdl/wavgenDriver.py index f29a12a..6a1eea9 100755 --- a/wdl/wavgenDriver.py +++ b/wdl/wavgenDriver.py @@ -33,7 +33,16 @@ # David Hale or # Stephen Kaye -import wavgen +#hack to allow both modern style import and direct script execution +if __package__ != "wdl": + import os + import warnings + basen = os.path.basename(__file__) + warnings.warn(f"detected running a script directly, consider using python -m wdl.{basen}") + import wavgen +else: + from . import wavgen + import matplotlib.pyplot as plt import sys diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index 2d733b8..3e2c8d7 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -52,9 +52,19 @@ # for backwards compatibility with python 2 +#hack to allow both modern style import and direct script execution +if __package__ != "wdl": + import os + import warnings + basen = os.path.basename(__file__) + warnings.warn(f"detected running a script directly, consider using python -m wdl.{basen}") + import Lexer + from Symbols import * +else: + from . import Lexer + from .Symbols import * + -import Lexer -from Symbols import * import sys sys.dont_write_bytecode = True @@ -222,7 +232,7 @@ def module(): "ADF": 13, "ADX": 14, "ADLN": 15, - "DRIVERX": 16, + "DRIVERX": 16 } mod_type = module_map.get(module_name.upper(), -1) @@ -546,7 +556,10 @@ def clamp(slot_number): ad_chan = token.cargo if int(ad_chan) < 1 or int(ad_chan) > 4: error("CLAMP channel " + dq(ad_chan) + " outside range [1..4]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + ad_chan = token.cargo + consume(IDENTIFIER) _clamp = None consume("=") while not found(";"): @@ -561,7 +574,10 @@ def clamp(slot_number): if found(NUMBER): _clamp = sign * float(token.cargo) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + _clamp = sign * float(token.cargo) + consume(IDENTIFIER) consume(";") adcOutput += "MOD" + slot_number + "\CLAMP" + ad_chan + "=" + str(_clamp) + "\n" @@ -590,8 +606,11 @@ def hvhc(slot_number): hvh_chan = token.cargo if not (1 <= int(hvh_chan) <= 6): error(f"HVHC channel {dq(hvh_chan)} outside range [1..6]") + consume(NUMBER) + elif found(IDENTIFIER): + hvh_chan = token.cargo + consume(IDENTIFIER) - consume(NUMBER) volts = None current = None order = None @@ -605,25 +624,37 @@ def hvhc(slot_number): volts = token.cargo if float(volts) < 0 or float(volts) > 31: error("HVHC volts " + dq(volts) + " outside range [0..31] V") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + volts = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): current = token.cargo if float(current) < 0 or float(current) > 250: error("HVHC current " + dq(current) + " outside range [0..250] mA") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + current = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): order = token.cargo if int(order) < 0 or int(order) > 6: error("HVHC order " + dq(order) + " outside range [0..6]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + order = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): enable = token.cargo if enable != "0" and enable != "1": error("HVHC enable " + dq(enable) + " must be 0 or 1") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + enable = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING @@ -665,7 +696,10 @@ def hvlc(slot_number): hvl_chan = token.cargo if int(hvl_chan) < 1 or int(hvl_chan) > 24: error("HVLC channel " + dq(hvl_chan) + " outside range [1..24]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + hvl_chan = token.cargo + consume(IDENTIFIER) volts = None order = None consume("[") @@ -676,13 +710,19 @@ def hvlc(slot_number): volts = token.cargo if float(volts) < 0 or float(volts) > 31: error("HVLC volts " + dq(volts) + " outside range [0..31] V") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + volts = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): order = token.cargo if int(order) < 0 or int(order) > 24: error("HVLC order " + dq(order) + " outside range [0..24]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + order = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING if found(STRING): @@ -721,7 +761,11 @@ def lvhc(slot_number): lvh_chan = token.cargo if int(lvh_chan) < 1 or int(lvh_chan) > 6: error("LVHC channel " + dq(lvh_chan) + " outside range [1..6]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + lvh_chan = token.cargo + consume(IDENTIFIER) + volts = None current = None order = None @@ -739,25 +783,37 @@ def lvhc(slot_number): volts = sign * float(token.cargo) if float(volts) < -14 or float(volts) > 14: error("LVHC volts " + dq(str(volts)) + " outside range [-14..14] V") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + volts = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): current = token.cargo if float(current) < 0 or float(current) > 250: error("LVHC current " + dq(current) + " outside range [0..250] mA") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + current = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): order = token.cargo if int(order) < 0 or int(order) > 6: error("LVHC order " + dq(order) + " outside range [0..6]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + order = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): enable = token.cargo if enable != "0" and enable != "1": error("LVHC enable " + dq(enable) + " must be 0 or 1") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + enable = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING if found(STRING): @@ -798,7 +854,10 @@ def lvlc(slot_number): lvl_chan = token.cargo if int(lvl_chan) < 1 or int(lvl_chan) > 24: error("LVLC channel " + dq(lvl_chan) + " outside range [1..24]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + lvl_chan = token.cargo + consume(IDENTIFIER) volts = None order = None consume("[") @@ -816,13 +875,19 @@ def lvlc(slot_number): volts = sign * float(token.cargo) if volts < -14 or volts > 14: error("LVLC volts " + dq(str(volts)) + " outside range [-14..14] V") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + volts = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): order = token.cargo if int(order) < 0 or int(order) > 24: error("LVLC order " + dq(order) + " outside range [0..24]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + order = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING if found(STRING): @@ -876,11 +941,17 @@ def sensor(slot_number): sensorType = token.cargo if int(sensorType) < 0 or int(sensorType) > 5: error("SENSOR type: " + sensorType + ": must be in range {0:5}") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + sensorType = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): sensor_current = token.cargo - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + sensor_current = token.cargo + consume(IDENTIFIER) consume(",") # sensor_lo_lim could have a negative number... sign = +1 @@ -895,7 +966,10 @@ def sensor(slot_number): + str(sensor_lo_lim) + ": must be in range {-150:50} deg C" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + sensor_lo_lim = token.cargo + consume(IDENTIFIER) consume(",") # sensor_hi_lim could have a negative number... sign = +1 @@ -910,13 +984,19 @@ def sensor(slot_number): + str(sensor_hi_lim) + ": must be in range {-150:50} deg C" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + sensor_hi_lim = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): sensor_filter = token.cargo if int(sensor_filter) < 0 or int(sensor_filter) > 8: error("SENSOR filter " + sensor_filter + ": must be in range {0:8}") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + sensor_filter = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING if found(STRING): @@ -1008,19 +1088,31 @@ def pid(slot_number): break if found(NUMBER): heater_p = token.cargo - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + heater_p = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): heater_i = token.cargo - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + heater_i = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): heater_d = token.cargo - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + heater_d = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): heater_ilim = token.cargo - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + heater_ilim = token.cargo + consume(IDENTIFIER) consume("]") consume(";") @@ -1073,7 +1165,10 @@ def ramp(slot_number): ramprate = int(token.cargo) if ramprate < 1 or ramprate > 32767: error("RAMP rate " + dq(str(ramprate)) + " must be in range {1:32767}") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + ramprate = token.cargo + consume(IDENTIFIER) consume(",") # if it's a number then allow a 0 or 1 if found(NUMBER): @@ -1162,7 +1257,10 @@ def heater(slot_number): + str(heater_target) + ": must be in range {-150:50}" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + heater_target = token.cargo + consume(IDENTIFIER) consume(",") # heater_sensor if found(IDENTIFIER): @@ -1186,7 +1284,10 @@ def heater(slot_number): + str(heater_limit) + ": must be in range {0:25}" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + heater_limit = token.cargo + consume(IDENTIFIER) consume(",") # heater_forcelevel if found(NUMBER): @@ -1197,7 +1298,10 @@ def heater(slot_number): + str(heater_forcelevel) + ": must be in range {0:25}" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + heater_forcelevel = token.cargo + consume(IDENTIFIER) consume(",") # heater_force # if it's a number then allow a 0 or 1 @@ -1357,13 +1461,19 @@ def pbias(slot_number): bias_chan = token.cargo if int(bias_chan) < 1 or int(bias_chan) > 4: error("PBIAS channel " + dq(bias_chan) + " outside range {1:4}") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + bias_chan = token.cargo + consume(IDENTIFIER) if found(NUMBER): enable = token.cargo if int(enable) < 0 or int(enable) > 1: error("PBIAS enable " + dq(enable) + " must be 0 or 1") consume(NUMBER) + elif found(IDENTIFIER): + enable = token.cargo + consume(IDENTIFIER) else: enable = "1" @@ -1377,13 +1487,19 @@ def pbias(slot_number): order = token.cargo if int(order) < 0: error("PBIAS order " + order + " must be >= 0") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + order = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): cmd = token.cargo - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + cmd = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING @@ -1429,13 +1545,19 @@ def nbias(slot_number): bias_chan = token.cargo if int(bias_chan) < 1 or int(bias_chan) > 4: error("NBIAS channel " + dq(bias_chan) + " outside range {1:4}") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + bias_chan = token.cargo + consume(IDENTIFIER) if found(NUMBER): enable = token.cargo if int(enable) < 0 or int(enable) > 1: error("NBIAS enable " + dq(enable) + " must be 0 or 1") consume(NUMBER) + elif found(IDENTIFIER): + enable = token.cargo + consume(IDENTIFIER) else: enable = "1" @@ -1449,14 +1571,19 @@ def nbias(slot_number): order = token.cargo if int(order) < 0: error("NBIAS order " + order + " must be >= 0") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + order = token.cargo + consume(IDENTIFIER) consume(",") # must be a negative number... - consume("-") if found(NUMBER): cmd = token.cargo + + if float(cmd) > 0.0: + error("XVN value must be negative!") consume(NUMBER) consume("]") @@ -1468,7 +1595,7 @@ def nbias(slot_number): label = "" consume(";") - nbiasOutput += "MOD" + slot_number + "\XVN_V" + bias_chan + "=-" + cmd + "\n" + nbiasOutput += "MOD" + slot_number + "\XVN_V" + bias_chan + "=" + cmd + "\n" nbiasOutput += "MOD" + slot_number + "\XVN_ORDER" + bias_chan + "=" + order + "\n" nbiasOutput += "MOD" + slot_number + "\XVN_ENABLE" + bias_chan + "=" + enable + "\n" if label != "": @@ -1500,7 +1627,10 @@ def drv(slot_number): drv_chan = token.cargo if int(drv_chan) < 0 or int(drv_chan) > 8: error("DRV channel " + dq(drv_chan) + " outside range [1..8]") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + drv_chan = token.cargo + consume(IDENTIFIER) slewfast = None slewslow = None enable = None @@ -1516,7 +1646,10 @@ def drv(slot_number): + dq(slewfast) + " outside range [0.001..1000] V/us" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + slewfast = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): slewslow = token.cargo @@ -1526,13 +1659,19 @@ def drv(slot_number): + dq(slewslow) + " outside range [0.001..1000] V/us" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + slewslow = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): enable = token.cargo if enable != "0" and enable != "1": error("DRV enable " + dq(enable) + " must be 0 or 1") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + enable = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING if found(STRING): @@ -1576,8 +1715,11 @@ def drvx(slot_number): drv_chan = token.cargo if int(drv_chan) < 0 or int(drv_chan) > 12: error(f"DRVX channel {dq(drv_chan)} outside range [1..12]") + consume(NUMBER) + elif found(IDENTIFIER): + drv_chan = token.cargo + consume(IDENTIFIER) - consume(NUMBER) slewfast = None slewslow = None enable = None @@ -1589,7 +1731,10 @@ def drvx(slot_number): slewfast = token.cargo if float(slewfast) < 0.001 or float(slewfast) > 1000: error(f"DRVX Slow Slew Rate {dq(slewslow)} outside range [0.001..1000] V/us") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + slewfast = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): slewslow = token.cargo @@ -1599,13 +1744,19 @@ def drvx(slot_number): + dq(slewslow) + " outside range [0.001..1000] V/us" ) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + slewslow = token.cargo + consume(IDENTIFIER) consume(",") if found(NUMBER): enable = token.cargo if enable != "0" and enable != "1": error(f"DRVX enable {dq(enable)} must be 0 or 1") - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + enable = token.cargo + consume(IDENTIFIER) consume("]") # there can be an optional label, specified as token type=STRING label = "" @@ -1652,7 +1803,7 @@ def slot(): slot_number = token.cargo if int(slot_number) < 1 or int(slot_number) > 12: error("SLOT " + dq(slot_number) + " outside range [1..12]") - consume(NUMBER) + consume(NUMBER) mod_type = None if found(IDENTIFIER): @@ -1909,17 +2060,12 @@ def to(): global setLevel consume("TO") - # could be a negative number... - if found("-"): - # if so, consume the sign and remember it - consume("-") - sign = -1.0 - else: - sign = 1.0 if found(NUMBER): - # multiply the value by the sign from above (hehe) - setLevel = str(sign * float(token.cargo)) - consume(NUMBER) + setLevel = float(token.cargo) + consume(NUMBER) + elif found(IDENTIFIER): + setLevel = token.cargo + consume(IDENTIFIER) # ----------------------------------------------------------------------------- @@ -2261,7 +2407,8 @@ def generic_sequence(*sequenceName): if len(sequence_line) > 0: output_text += sequence_line + "\n" # do we have an exit from this sequence? - if not has_exit: + # exception for special "Start" sequence + if not has_exit and not 'Start' in sequenceName: error("(wdlParser.py::generic_sequence) no exit from sequence " + sequenceName[0]) # sequence/waveform must end with a close (right) curly brace, } diff --git a/wdl/wdlParserDriver.py b/wdl/wdlParserDriver.py index 55fd238..ecca753 100755 --- a/wdl/wdlParserDriver.py +++ b/wdl/wdlParserDriver.py @@ -22,8 +22,31 @@ # David Hale or # Stephen Kaye + +#hack to allow both modern style import and direct script execution +if __package__ in (None, ""): + import sys + import warnings + from pathlib import Path + + # Add the repo/package parent directory so `import wdl...` works + # even when this file is run directly. + repo_root = Path(__file__).resolve().parent.parent + sys.path.insert(0, str(repo_root)) + + module_name = Path(__file__).stem + warnings.warn( + f"Detected direct script execution. " + f"Consider using: python -m wdl.{module_name}", + RuntimeWarning, + stacklevel=2, + ) + + from wdl import wdlParser as Parser +else: + from . import wdlParser as Parser + import fileinput -import wdlParser as Parser import sys sys.dont_write_bytecode = True