From 0dc65a9555d561dc8875ce5b469823edbd9be237 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 28 Feb 2025 11:44:53 -0800 Subject: [PATCH 01/34] start on a unified CLI callable for WDL --- wdl/cli.py | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 wdl/cli.py diff --git a/wdl/cli.py b/wdl/cli.py new file mode 100644 index 0000000..394b76c --- /dev/null +++ b/wdl/cli.py @@ -0,0 +1,194 @@ +from argparse import ArgumentParser, _SubParsersAction, Namespace +from abc import ABCMeta, abstractmethod +from dataclasses import dataclass +from sys import stdin +from typing import Optional, ContextManager +from contextlib import contextmanager +from io import TextIOWrapper +import warnings + +from wdl.wdlParser import make_include_sequence, parse_modules, parse_system, make_include +import wdl.wavgen as wavgen +import wdl.modegen as modegen +import fileinput +import logging +import matplotlib.pyplot as plt + +logger = logging.getLogger(__name__) + +class WDLDriver(metaclass=ABCMeta): + @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 runner(args: Namespace) -> None: + kwargs = vars(args) + obj: cls = cls(**kwargs) + obj() + + parser.set_defaults(func=runner) + return parser + + 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) -> None: ... + + @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() + + def _read_file_or_stdin(self, fname: str) -> str: + with self._file_or_stdin(fname) as f: + return f.read() + + +class SeqParserDriver(WDLDriver): + CMD_NAME: str = "seq" + CMD_DESCRIPTION: str = "parse a .conf file and make an include list from it" + + def __call__(self) -> None: + logger.info("making include sequence") + make_include_sequence(self._text) + +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... This is fragile behaviour, ideally please use the --name argument to the command instead") + projname = f.readline() + + logger.info("project name is: %s", projname) + self._projname: str = projname + self._text: str = f.read() + + def __call__(self) -> None: + 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()) + + + def _write_output(self, archonkw: str, fileext: str, output: str) -> None: + fname: str = f"{self._projname}.{fileext}" + with open(fname, "w") as f: + f.writelines([f"[{archonkw}]"]) + f.write(output) + +class IncParserDriver(WDLDriver): + CMD_NAME: str = "inc" + CMD_DESCRIPTION: str = "parse an include file" + + def __call__(self) -> None: + make_include(self._text) + +class WdlParserDriver(WDLDriver): + CMD_NAME: str = "wdl" + CMD_DESCRIPTION: str = "parse the subroutines from a WDL input file" + + def __call__(self) -> None: + logger.info("parsing WDL file...") + output = parse(self._text) + + +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): + #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 + #global variable because OF COURSE IT IS + self._fname: str = fname + self._plots: bool = plots + + def __call__(self) -> None: + 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) + +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): + self._modefile = modefile + self._acffile = acffile + + def __call__(self) -> None: + modegen.Modegen(self._modefile, self._acffile) + +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") + + for cls in [SeqParserDriver, ModParserDriver, IncParserDriver, WdlParserDriver, + WavgenDriver, ModegenDriver]: + cls.setup_subparser(subparsers) + + args = ap.parse_args() + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + #call the command + args.func(args) + + +if __name__ == "__main__": + main() From 7b34d3a7844b83b6658462b746c8f5469c1b014c Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 28 Feb 2025 15:39:10 -0800 Subject: [PATCH 02/34] clarify a comment --- wdl/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wdl/cli.py b/wdl/cli.py index 394b76c..b2f4c7f 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -135,11 +135,12 @@ def __init__(self, fname: str, plots: bool): #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 - #global variable because OF COURSE IT IS + self._fname: str = fname self._plots: bool = plots def __call__(self) -> None: + #global variable because OF COURSE IT IS wavgen.GenerateFigs = self._plots wavgen.loadWDL(f"{self._fname}.wdl", self._fname) From 4799da047046cda3c7a65d7e22d0b35aef1bb691 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 28 Feb 2025 15:39:21 -0800 Subject: [PATCH 03/34] initial attempt at a replacement for ini2acf.pl --- wdl/ini2acf.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 wdl/ini2acf.py diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py new file mode 100644 index 0000000..c0b5163 --- /dev/null +++ b/wdl/ini2acf.py @@ -0,0 +1,73 @@ +"""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 + +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]]]: + #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 + seclines: list[str] = [] + for line in inp: + 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, process + thissection = secname + seclines.clear() + +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] = [] + for secname, seclines, process in _section_replace_filter(inp): + outp.append(f"[{secname}]") + 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.extend(seclines) + return outp + + +if __name__ == "__main__": + #basic direct call behaviour to mimic the old ini2acf.pl script + # TODO + pass + From 3a11426de62a9a6ddd7670af3f5ee5d4bcabe68f Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Wed, 5 Mar 2025 17:05:40 -0800 Subject: [PATCH 04/34] script installs and finish initial ini2acf replacement --- pyproject.toml | 10 ++++++++++ wdl/Lexer.py | 8 ++++---- wdl/LexerDriver.py | 4 ++-- wdl/genericScanner.py | 2 +- wdl/ini2acf.py | 44 ++++++++++++++++++++++++++++++++++++++++--- wdl/wdlParser.py | 4 ++-- 6 files changed, 60 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 49690ac..970859c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,3 +23,13 @@ dependencies = [ [project.urls] Repository = "https://github.com/CaltechOpticalObservatories/wdl" + +[project.scripts] +# the "new" command line tool +wdl = "wdl.cli:main" +ini2acf = "wdl.ini2acf:main" + +#command script wrappers for legacy .py "Driver" scripts +"LexerDriver.py" = "wdl.LexerDriver:__main__" + + diff --git a/wdl/Lexer.py b/wdl/Lexer.py index 6e075fc..0ad3b73 100644 --- a/wdl/Lexer.py +++ b/wdl/Lexer.py @@ -29,10 +29,10 @@ # David Hale or # Stephen Kaye -import genericScanner as Scanner -from genericToken import * -from Symbols import * -from genericCharacter import * +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/genericScanner.py b/wdl/genericScanner.py index 050d52e..4fdf03b 100644 --- a/wdl/genericScanner.py +++ b/wdl/genericScanner.py @@ -27,7 +27,7 @@ # David Hale or # Stephen Kaye -from genericCharacter import * +from .genericCharacter import * """ A Scanner object reads through the source_text diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index c0b5163..6b7c93a 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Generator, Optional import sys +from argparse import ArgumentParser def _extract_section_name(line: str) -> Optional[tuple[str, bool]]: if '[' not in line: @@ -15,7 +16,7 @@ def _extract_section_name(line: str) -> Optional[tuple[str, bool]]: return secname.strip('#'), True -def _section_replace_filter(inp: str) -> Generator[tuple[str, list[str]]]: +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 @@ -65,9 +66,46 @@ def generate_acf(inifile: str | Path | TextIOWrapper, outp.extend(seclines) return outp +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.inifile) + + if args.outfile is not None: + with open(args.outfile,"w") as f: + f.write(out_content) + + else: + sys.stdout.write(out_content) + + if __name__ == "__main__": #basic direct call behaviour to mimic the old ini2acf.pl script # TODO - pass - + main() + diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index 2d733b8..b81d4b7 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -53,8 +53,8 @@ # for backwards compatibility with python 2 -import Lexer -from Symbols import * +from . import Lexer +from .Symbols import * import sys sys.dont_write_bytecode = True From 8220c16eda02088dceb37ba1ef91606a44745805 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Thu, 6 Mar 2025 11:33:33 -0800 Subject: [PATCH 05/34] add minimum required python --- pyproject.toml | 8 ++++++++ wdl/incParserDriver.py | 2 +- wdl/wdlParserDriver.py | 6 +++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 970859c..c1fc580 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,14 @@ dependencies = [ "matplotlib>=3.9.2", "PyQt5>=5.15" ] + + +#note this is a GUESS. With agreement from COO software people (I need context for this) +#I would like to bump this sooner. Note python 3.8 was released in 2019, and is the default python for e.g. ubuntu 20.04 +#On ubuntu 20.04, python 3.10 is available from "official" sources and as recent as python 3.12 is available from PPAs. +#This is not including, of course, installing a conda distribution to get python whatever. +requires-python = ">=3.8" + [project.urls] Repository = "https://github.com/CaltechOpticalObservatories/wdl" diff --git a/wdl/incParserDriver.py b/wdl/incParserDriver.py index 6d283c4..77d00ca 100755 --- a/wdl/incParserDriver.py +++ b/wdl/incParserDriver.py @@ -24,7 +24,7 @@ import fileinput -import wdlParser as Parser +from . import wdlParser as Parser import sys sys.dont_write_bytecode = True diff --git a/wdl/wdlParserDriver.py b/wdl/wdlParserDriver.py index 55fd238..1efec1b 100755 --- a/wdl/wdlParserDriver.py +++ b/wdl/wdlParserDriver.py @@ -22,8 +22,12 @@ # David Hale or # Stephen Kaye + + + + import fileinput -import wdlParser as Parser +from . import wdlParser as Parser import sys sys.dont_write_bytecode = True From c841ef11d7a13a5ea1cdbe235d95e87035e0f9d0 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Thu, 6 Mar 2025 17:04:06 -0800 Subject: [PATCH 06/34] allow executing wdl directly as a module --- wdl/__main__.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 wdl/__main__.py 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() From 7653bb9badbc58448879aef6f039476614e359a9 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Thu, 6 Mar 2025 17:26:08 -0800 Subject: [PATCH 07/34] add ini2acf subcommand to new CLI app --- wdl/cli.py | 45 ++++++++++++++++++++++++++++++++++++++++-- wdl/modParserDriver.py | 2 +- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/wdl/cli.py b/wdl/cli.py index b2f4c7f..d7a44ba 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -1,18 +1,21 @@ from argparse import ArgumentParser, _SubParsersAction, Namespace from abc import ABCMeta, abstractmethod from dataclasses import dataclass -from sys import stdin +from sys import stdin, stdout from typing import Optional, ContextManager from contextlib import contextmanager from io import TextIOWrapper import warnings from wdl.wdlParser import make_include_sequence, parse_modules, parse_system, make_include +from wdl.ini2acf import generate_acf import wdl.wavgen as wavgen import wdl.modegen as modegen import fileinput import logging import matplotlib.pyplot as plt +from pathlib import Path, glob +import os logger = logging.getLogger(__name__) @@ -37,6 +40,16 @@ def runner(args: Namespace) -> None: parser.set_defaults(func=runner) 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""" @@ -55,6 +68,14 @@ def _file_or_stdin(self, fname: str) -> ContextManager[TextIOWrapper]: 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() @@ -170,6 +191,26 @@ def __init__(self, modefile: str, acffile: str): def __call__(self) -> None: modegen.Modegen(self._modefile, self._acffile) +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]): + super().__init__(fname) + self._outfile = sys.stdout if outfile is None else outfile + + def __call__(self) -> None: + outtxt: str = generate_acf(self._text, treat_str_as_content=True) + with self._file_or_stdout(self._outfile) as f: + f.write(outtxt) + + def main(): ap = ArgumentParser(prog="wdl", description="command line interface to Waveform Definition Language (WDL)") @@ -177,7 +218,7 @@ def main(): subparsers = ap.add_subparsers(required=True, help="the WDL subcommand to run") for cls in [SeqParserDriver, ModParserDriver, IncParserDriver, WdlParserDriver, - WavgenDriver, ModegenDriver]: + WavgenDriver, ModegenDriver, Ini2acfDriver]: cls.setup_subparser(subparsers) args = ap.parse_args() diff --git a/wdl/modParserDriver.py b/wdl/modParserDriver.py index eee328b..da759cd 100755 --- a/wdl/modParserDriver.py +++ b/wdl/modParserDriver.py @@ -24,7 +24,7 @@ import fileinput -import wdlParser as Parser +from . import wdlParser as Parser import sys sys.dont_write_bytecode = True From 694ccf831e2ee7ddd72f211cbf618497a3c804d6 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 7 Mar 2025 14:55:35 -0800 Subject: [PATCH 08/34] fix (very bad) typo in pyproject.toml definition --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c1fc580..44571e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ 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"}] From 07387ddb26dfb4377a62b9821531ffc31c607d29 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 7 Mar 2025 14:59:35 -0800 Subject: [PATCH 09/34] glob is not a function in pathlib, but a function of Path --- wdl/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wdl/cli.py b/wdl/cli.py index d7a44ba..6a4f008 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -14,7 +14,7 @@ import fileinput import logging import matplotlib.pyplot as plt -from pathlib import Path, glob +from pathlib import Path import os logger = logging.getLogger(__name__) From 1f6b5b5f51813c3804c3623606ef1e1beb5a2151 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 7 Mar 2025 15:21:59 -0800 Subject: [PATCH 10/34] refactor CLI command code --- wdl/cli.py | 216 ++------------------------------- wdl/commands/__init__.py | 0 wdl/commands/driverbase.py | 67 ++++++++++ wdl/commands/legacy_drivers.py | 160 ++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 209 deletions(-) create mode 100644 wdl/commands/__init__.py create mode 100644 wdl/commands/driverbase.py create mode 100644 wdl/commands/legacy_drivers.py diff --git a/wdl/cli.py b/wdl/cli.py index 6a4f008..36d6af8 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -1,216 +1,14 @@ -from argparse import ArgumentParser, _SubParsersAction, Namespace -from abc import ABCMeta, abstractmethod -from dataclasses import dataclass -from sys import stdin, stdout -from typing import Optional, ContextManager -from contextlib import contextmanager -from io import TextIOWrapper -import warnings +from argparse import ArgumentParser -from wdl.wdlParser import make_include_sequence, parse_modules, parse_system, make_include -from wdl.ini2acf import generate_acf -import wdl.wavgen as wavgen -import wdl.modegen as modegen -import fileinput -import logging -import matplotlib.pyplot as plt -from pathlib import Path -import os - -logger = logging.getLogger(__name__) - -class WDLDriver(metaclass=ABCMeta): - @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 runner(args: Namespace) -> None: - kwargs = vars(args) - obj: cls = cls(**kwargs) - obj() - - parser.set_defaults(func=runner) - 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) -> None: ... - - @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() - - -class SeqParserDriver(WDLDriver): - CMD_NAME: str = "seq" - CMD_DESCRIPTION: str = "parse a .conf file and make an include list from it" - - def __call__(self) -> None: - logger.info("making include sequence") - make_include_sequence(self._text) - -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... This is fragile behaviour, ideally please use the --name argument to the command instead") - projname = f.readline() - - logger.info("project name is: %s", projname) - self._projname: str = projname - self._text: str = f.read() - - def __call__(self) -> None: - logger.debug("writing output to .modules file...") - self._write_output("CONFIG", "modules", parse_modules(self._text)) +from .commands.legacy_drivers import ( + SeqParserDriver, ModParserDriver, IncParserDriver, + WdlParserDriver, WavgenDriver, ModegenDriver, Ini2acfDriver) - #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()) +import logging - def _write_output(self, archonkw: str, fileext: str, output: str) -> None: - fname: str = f"{self._projname}.{fileext}" - with open(fname, "w") as f: - f.writelines([f"[{archonkw}]"]) - f.write(output) - -class IncParserDriver(WDLDriver): - CMD_NAME: str = "inc" - CMD_DESCRIPTION: str = "parse an include file" - - def __call__(self) -> None: - make_include(self._text) - -class WdlParserDriver(WDLDriver): - CMD_NAME: str = "wdl" - CMD_DESCRIPTION: str = "parse the subroutines from a WDL input file" - - def __call__(self) -> None: - logger.info("parsing WDL file...") - output = parse(self._text) - - -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): - #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) -> None: - #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) - -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): - self._modefile = modefile - self._acffile = acffile - - def __call__(self) -> None: - modegen.Modegen(self._modefile, self._acffile) - -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]): - super().__init__(fname) - self._outfile = sys.stdout if outfile is None else outfile - - def __call__(self) -> None: - outtxt: str = generate_acf(self._text, treat_str_as_content=True) - with self._file_or_stdout(self._outfile) as f: - f.write(outtxt) +logger = logging.getLogger(__name__) - def main(): ap = ArgumentParser(prog="wdl", description="command line interface to Waveform Definition Language (WDL)") @@ -228,7 +26,7 @@ def main(): else: logging.basicConfig(level=logging.INFO) - #call the command + # call the command args.func(args) 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..ffcca4c --- /dev/null +++ b/wdl/commands/driverbase.py @@ -0,0 +1,67 @@ +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 + +class WDLDriver(metaclass=ABCMeta): + @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 runner(args: Namespace) -> None: + kwargs = vars(args) + obj: cls = cls(**kwargs) + obj() + + parser.set_defaults(func=runner) + 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) -> None: ... + + @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..f74b80d --- /dev/null +++ b/wdl/commands/legacy_drivers.py @@ -0,0 +1,160 @@ +import logging +import warnings +from typing import Optional +from argparse import ArgumentParser +from sys import stdout + +from ..cli 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, 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) -> None: + logger.info("making include sequence") + make_include_sequence(self._text) + + +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... This is fragile behaviour, ideally please use the --name argument to the command instead") + projname = f.readline() + + logger.info("project name is: %s", projname) + self._projname: str = projname + self._text: str = f.read() + + def __call__(self) -> None: + 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()) + + + def _write_output(self, archonkw: str, fileext: str, output: str) -> None: + fname: str = f"{self._projname}.{fileext}" + with open(fname, "w") as f: + f.writelines([f"[{archonkw}]"]) + f.write(output) + +class IncParserDriver(WDLDriver): + CMD_NAME: str = "inc" + CMD_DESCRIPTION: str = "parse an include file" + + def __call__(self) -> None: + make_include(self._text) + +class WdlParserDriver(WDLDriver): + CMD_NAME: str = "wdl" + CMD_DESCRIPTION: str = "parse the subroutines from a WDL input file" + + def __call__(self) -> None: + 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) + + +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): + #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) -> None: + # 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) + +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): + self._modefile = modefile + self._acffile = acffile + + def __call__(self) -> None: + modegen.Modegen(self._modefile, self._acffile) + +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]): + super().__init__(fname) + self._outfile = sys.stdout if outfile is None else outfile + + def __call__(self) -> None: + outtxt: str = generate_acf(self._text, treat_str_as_content=True) + with self._file_or_stdout(self._outfile) as f: + f.write(outtxt) From 27d08ae694691fce65d67651ca96831ac86ac2d2 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 7 Mar 2025 15:32:20 -0800 Subject: [PATCH 11/34] start on preprocessor utility commands --- wdl/cli.py | 4 ++-- wdl/commands/legacy_drivers.py | 15 +++++++++++---- wdl/commands/preprocess.py | 19 +++++++++++++++++++ wdl/ini2acf.py | 5 +++-- 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 wdl/commands/preprocess.py diff --git a/wdl/cli.py b/wdl/cli.py index 36d6af8..cb21061 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -27,8 +27,8 @@ def main(): logging.basicConfig(level=logging.INFO) # call the command - args.func(args) + return args.func(args) if __name__ == "__main__": - main() + return main() diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py index f74b80d..4333aea 100644 --- a/wdl/commands/legacy_drivers.py +++ b/wdl/commands/legacy_drivers.py @@ -25,9 +25,10 @@ class SeqParserDriver(WDLDriver): CMD_NAME: str = "seq" CMD_DESCRIPTION: str = "parse a .conf file and make an include list from it" - def __call__(self) -> None: + def __call__(self) -> int: logger.info("making include sequence") make_include_sequence(self._text) + return 0 class ModParserDriver(WDLDriver): @@ -59,7 +60,7 @@ def __call__(self) -> None: # 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) -> None: fname: str = f"{self._projname}.{fileext}" @@ -73,6 +74,7 @@ class IncParserDriver(WDLDriver): def __call__(self) -> None: make_include(self._text) + return 0 class WdlParserDriver(WDLDriver): CMD_NAME: str = "wdl" @@ -89,7 +91,7 @@ def __call__(self) -> None: #apparently this one just prints it out, to stdout I guess? stdout.write(output) - + return 0 class WavgenDriver(WDLDriver): CMD_NAME: str = "wavgen" @@ -121,6 +123,9 @@ def __call__(self) -> None: 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" @@ -139,6 +144,7 @@ def __init__(self, modefile: str, acffile: str): def __call__(self) -> None: modegen.Modegen(self._modefile, self._acffile) + return 0 class Ini2acfDriver(WDLDriver): CMD_NAME: str = "ini2acf" @@ -152,9 +158,10 @@ def setup_subparser(cls, subparsers) -> ArgumentParser: def __init__(self, fname: str, outfile: Optional[str]): super().__init__(fname) - self._outfile = sys.stdout if outfile is None else outfile + self._outfile = stdout if outfile is None else outfile def __call__(self) -> None: outtxt: str = generate_acf(self._text, treat_str_as_content=True) with self._file_or_stdout(self._outfile) as f: f.write(outtxt) + return 0 diff --git a/wdl/commands/preprocess.py b/wdl/commands/preprocess.py new file mode 100644 index 0000000..3dcd1c5 --- /dev/null +++ b/wdl/commands/preprocess.py @@ -0,0 +1,19 @@ +from .driverbase import WDLDriver +from argparse import ArgumentParser +from sys import stdout +from shutil import which +import warnings + + +class FindGPP(WDLDriver): + @classmethod + def setup_subparser(cls, subparsers) -> ArgumentParser: + return super().setup_subparser(subparsers, fname_arg_setup=False) + + def __init__(self): + #For now, just find system gpp, in future we can check bundled as well + self._gpppath = which("gpp") + + def __call__(self) -> None: + + diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index 6b7c93a..a45e10a 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -101,11 +101,12 @@ def main(): else: sys.stdout.write(out_content) - + + return 0 if __name__ == "__main__": #basic direct call behaviour to mimic the old ini2acf.pl script # TODO - main() + return main() From 11d8817838de05d55d5a085e4d20562035827e88 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 10 Mar 2025 16:18:32 -0700 Subject: [PATCH 12/34] more minor driver CLI fixes --- wdl/cli.py | 8 ++++++-- wdl/commands/driverbase.py | 1 + wdl/commands/legacy_drivers.py | 19 ++++++++++--------- wdl/commands/preprocess.py | 17 +++++++++++++---- wdl/ini2acf.py | 3 ++- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/wdl/cli.py b/wdl/cli.py index cb21061..3ff24b5 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -1,9 +1,12 @@ from argparse import ArgumentParser +import sys from .commands.legacy_drivers import ( SeqParserDriver, ModParserDriver, IncParserDriver, WdlParserDriver, WavgenDriver, ModegenDriver, Ini2acfDriver) +from .commands.preprocess import FindGPP + import logging @@ -16,7 +19,7 @@ def main(): subparsers = ap.add_subparsers(required=True, help="the WDL subcommand to run") for cls in [SeqParserDriver, ModParserDriver, IncParserDriver, WdlParserDriver, - WavgenDriver, ModegenDriver, Ini2acfDriver]: + WavgenDriver, ModegenDriver, Ini2acfDriver, FindGPP]: cls.setup_subparser(subparsers) args = ap.parse_args() @@ -31,4 +34,5 @@ def main(): if __name__ == "__main__": - return main() + retcode = main() + sys.exit(retcode) diff --git a/wdl/commands/driverbase.py b/wdl/commands/driverbase.py index ffcca4c..a2f2210 100644 --- a/wdl/commands/driverbase.py +++ b/wdl/commands/driverbase.py @@ -4,6 +4,7 @@ from io import TextIOWrapper from sys import stdin from contextlib import contextmanager +from typing import ContextManager class WDLDriver(metaclass=ABCMeta): @classmethod diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py index 4333aea..737d205 100644 --- a/wdl/commands/legacy_drivers.py +++ b/wdl/commands/legacy_drivers.py @@ -4,7 +4,7 @@ from argparse import ArgumentParser from sys import stdout -from ..cli import WDLDriver +from .driverbase import WDLDriver from wdl.ini2acf import generate_acf @@ -12,7 +12,8 @@ # 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, Parser +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 @@ -52,7 +53,7 @@ def __init__(self, fname: str, projname: Optional[str] = None, **kwargs): self._projname: str = projname self._text: str = f.read() - def __call__(self) -> None: + def __call__(self) -> int: logger.debug("writing output to .modules file...") self._write_output("CONFIG", "modules", parse_modules(self._text)) @@ -62,7 +63,7 @@ def __call__(self) -> None: self._write_output("SYSTEM", "system", parse_system()) return 0 - def _write_output(self, archonkw: str, fileext: str, output: str) -> None: + def _write_output(self, archonkw: str, fileext: str, output: str) -> int: fname: str = f"{self._projname}.{fileext}" with open(fname, "w") as f: f.writelines([f"[{archonkw}]"]) @@ -72,7 +73,7 @@ class IncParserDriver(WDLDriver): CMD_NAME: str = "inc" CMD_DESCRIPTION: str = "parse an include file" - def __call__(self) -> None: + def __call__(self) -> int: make_include(self._text) return 0 @@ -80,7 +81,7 @@ class WdlParserDriver(WDLDriver): CMD_NAME: str = "wdl" CMD_DESCRIPTION: str = "parse the subroutines from a WDL input file" - def __call__(self) -> None: + def __call__(self) -> int: logger.info("parsing WDL file...") # TODO:blergh @@ -111,7 +112,7 @@ def __init__(self, fname: str, plots: bool): self._fname: str = fname self._plots: bool = plots - def __call__(self) -> None: + def __call__(self) -> int: # global variable because OF COURSE IT IS wavgen.GenerateFigs = self._plots wavgen.loadWDL(f"{self._fname}.wdl", self._fname) @@ -142,7 +143,7 @@ def __init__(self, modefile: str, acffile: str): self._modefile = modefile self._acffile = acffile - def __call__(self) -> None: + def __call__(self) -> int: modegen.Modegen(self._modefile, self._acffile) return 0 @@ -160,7 +161,7 @@ def __init__(self, fname: str, outfile: Optional[str]): super().__init__(fname) self._outfile = stdout if outfile is None else outfile - def __call__(self) -> None: + def __call__(self) -> int: outtxt: str = generate_acf(self._text, treat_str_as_content=True) with self._file_or_stdout(self._outfile) as f: f.write(outtxt) diff --git a/wdl/commands/preprocess.py b/wdl/commands/preprocess.py index 3dcd1c5..200d814 100644 --- a/wdl/commands/preprocess.py +++ b/wdl/commands/preprocess.py @@ -2,18 +2,27 @@ from argparse import ArgumentParser from sys import stdout from shutil import which +import os import warnings 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): + def __init__(self, **kwargs): #For now, just find system gpp, in future we can check bundled as well self._gpppath = which("gpp") - def __call__(self) -> None: - - + 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 diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index a45e10a..0f0b2d1 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -108,5 +108,6 @@ def main(): if __name__ == "__main__": #basic direct call behaviour to mimic the old ini2acf.pl script # TODO - return main() + retcode = main() + sys.exit(retcode) From c6ddfdec5e81444419d584e982ec4ab790eb1ad1 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 6 Jun 2025 12:40:14 -0700 Subject: [PATCH 13/34] add import hacks to all legacy drivers so they can be directly run inside or outside of a package verify that the old DEIMOS makefile and the new one work exactly the same --- wdl/Lexer.py | 19 +++++++++++++++---- wdl/cli.py | 1 + wdl/commands/legacy_drivers.py | 10 ++++++---- wdl/genericScanner.py | 12 +++++++++++- wdl/incParserDriver.py | 10 +++++++++- wdl/modParserDriver.py | 14 ++++++++++---- wdl/modegenDriver.py | 16 ++++++++++++++-- wdl/seqParserDriver.py | 11 ++++++++++- wdl/wavgenDriver.py | 11 ++++++++++- wdl/wdlParser.py | 14 ++++++++++++-- wdl/wdlParserDriver.py | 12 +++++++++--- 11 files changed, 107 insertions(+), 23 deletions(-) diff --git a/wdl/Lexer.py b/wdl/Lexer.py index 0ad3b73..aa25107 100644 --- a/wdl/Lexer.py +++ b/wdl/Lexer.py @@ -29,10 +29,21 @@ # David Hale or # Stephen Kaye -from . 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/cli.py b/wdl/cli.py index 3ff24b5..61c758a 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -24,6 +24,7 @@ def main(): args = ap.parse_args() + if args.debug: logging.basicConfig(level=logging.DEBUG) else: diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py index 737d205..bbf5699 100644 --- a/wdl/commands/legacy_drivers.py +++ b/wdl/commands/legacy_drivers.py @@ -47,7 +47,7 @@ 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... This is fragile behaviour, ideally please use the --name argument to the command instead") - projname = f.readline() + projname = f.readline().strip() logger.info("project name is: %s", projname) self._projname: str = projname @@ -65,6 +65,7 @@ def __call__(self) -> int: 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}]"]) f.write(output) @@ -104,7 +105,7 @@ def setup_subparser(cls, *args, **kwargs) -> ArgumentParser: parser.add_argument("--plots", action="store_true", help="generate plots to go with waveforms") return parser - def __init__(self, fname: str, plots: bool): + 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 @@ -139,7 +140,7 @@ def setup_subparser(cls, subparsers) -> ArgumentParser: parser.add_argument("acffile", type=str, help="the ACF file to append to") return parser - def __init__(self, modefile: str, acffile: str): + def __init__(self, modefile: str, acffile: str, **kwargs): self._modefile = modefile self._acffile = acffile @@ -157,12 +158,13 @@ def setup_subparser(cls, subparsers) -> ArgumentParser: 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]): + 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) -> 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/genericScanner.py b/wdl/genericScanner.py index 4fdf03b..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 77d00ca..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 -from . import wdlParser as Parser import sys sys.dont_write_bytecode = True diff --git a/wdl/modParserDriver.py b/wdl/modParserDriver.py index da759cd..7bcf0ca 100755 --- a/wdl/modParserDriver.py +++ b/wdl/modParserDriver.py @@ -23,13 +23,19 @@ # Stephen Kaye + import fileinput -from . 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__ != "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 # ----------------------------------------------------------------------------- # @fn main diff --git a/wdl/modegenDriver.py b/wdl/modegenDriver.py index 9c7db9c..f389099 100755 --- a/wdl/modegenDriver.py +++ b/wdl/modegenDriver.py @@ -32,9 +32,21 @@ # Stephen Kaye # import fileinput + +#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 + 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..f1a4cd6 100755 --- a/wdl/seqParserDriver.py +++ b/wdl/seqParserDriver.py @@ -22,8 +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/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 b81d4b7..b20014c 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 * + -from . import Lexer -from .Symbols import * import sys sys.dont_write_bytecode = True diff --git a/wdl/wdlParserDriver.py b/wdl/wdlParserDriver.py index 1efec1b..e92cd70 100755 --- a/wdl/wdlParserDriver.py +++ b/wdl/wdlParserDriver.py @@ -23,11 +23,17 @@ # 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 -from . import wdlParser as Parser import sys sys.dont_write_bytecode = True From a8088e59b38db2e6a40a2370f60154e4368793b5 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 6 Jun 2025 14:21:45 -0700 Subject: [PATCH 14/34] finish ini2acf script rewrite --- wdl/commands/legacy_drivers.py | 2 +- wdl/ini2acf.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py index bbf5699..adefdd9 100644 --- a/wdl/commands/legacy_drivers.py +++ b/wdl/commands/legacy_drivers.py @@ -46,7 +46,7 @@ def setup_subparser(cls, *args, **kwargs) -> ArgumentParser: 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... This is fragile behaviour, ideally please use the --name argument to the command instead") + 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) diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index 0f0b2d1..1e4445e 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -5,12 +5,15 @@ 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: + print(f"no section in line: {line}") return None else: secname: str = line.split("[")[1].split("]")[0] + print(f"found section: {secname}") if '#' not in secname: return secname, False return secname.strip('#'), True @@ -21,15 +24,19 @@ def _section_replace_filter(inp: str) -> Generator[tuple[str, list[str]], None, #way that even a physicist can understand here thissection: Optional[str] = None seclines: list[str] = [] - for line in inp: + 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 + print(f"section with name: {secname}") if thissection is not None: yield thissection, seclines, process thissection = secname seclines.clear() + else: + #this is a continuation of the previous section, just append the lines + seclines.append(line) def generate_acf(inifile: str | Path | TextIOWrapper, treat_str_as_content: bool=False) -> str: @@ -51,6 +58,7 @@ def generate_acf(inifile: str | Path | TextIOWrapper, #This is an already open file like opject inp: str = inifile.read() + print(f"input string length: {len(inp)}") outp: list[str] = [] for secname, seclines, process in _section_replace_filter(inp): outp.append(f"[{secname}]") @@ -64,7 +72,7 @@ def generate_acf(inifile: str | Path | TextIOWrapper, outp.append(f'{secname}S={ind+1}') else: outp.extend(seclines) - return outp + return f"{linesep.join(outp)}{linesep}" def main(): ap = ArgumentParser(prog="ini2acf", From 059e7b0121110413d6a1897af3b2a298c6a86998 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 6 Jun 2025 14:27:29 -0700 Subject: [PATCH 15/34] remove spurious extra prints. --- wdl/ini2acf.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index 1e4445e..bd5039f 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -9,11 +9,9 @@ def _extract_section_name(line: str) -> Optional[tuple[str, bool]]: if '[' not in line: - print(f"no section in line: {line}") return None else: secname: str = line.split("[")[1].split("]")[0] - print(f"found section: {secname}") if '#' not in secname: return secname, False return secname.strip('#'), True @@ -29,7 +27,6 @@ def _section_replace_filter(inp: str) -> Generator[tuple[str, list[str]], 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 - print(f"section with name: {secname}") if thissection is not None: yield thissection, seclines, process thissection = secname @@ -57,8 +54,6 @@ def generate_acf(inifile: str | Path | TextIOWrapper, else: #This is an already open file like opject inp: str = inifile.read() - - print(f"input string length: {len(inp)}") outp: list[str] = [] for secname, seclines, process in _section_replace_filter(inp): outp.append(f"[{secname}]") From b1bda95f5df8ae0eafa7d015eccb86239023dc6d Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 6 Jun 2025 15:13:52 -0700 Subject: [PATCH 16/34] work on building in gpp preprocessor --- wdl/cli.py | 37 +++++++++++++++++++++++++--------- wdl/commands/driverbase.py | 16 ++++++++------- wdl/commands/legacy_drivers.py | 14 ++++++------- wdl/commands/preprocess.py | 37 ++++++++++++++++++++++++++++++++-- 4 files changed, 79 insertions(+), 25 deletions(-) diff --git a/wdl/cli.py b/wdl/cli.py index 61c758a..a6f97ce 100644 --- a/wdl/cli.py +++ b/wdl/cli.py @@ -1,12 +1,11 @@ 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 - +from .commands.preprocess import FindGPP, PreprocessGPP import logging @@ -16,13 +15,18 @@ 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") + 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 [SeqParserDriver, ModParserDriver, IncParserDriver, WdlParserDriver, - WavgenDriver, ModegenDriver, Ini2acfDriver, FindGPP]: + for cls in command_classes: cls.setup_subparser(subparsers) - args = ap.parse_args() + args, unknown_args = ap.parse_known_args() if args.debug: @@ -30,8 +34,23 @@ def main(): else: logging.basicConfig(level=logging.INFO) - # call the command - return args.func(args) + #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__": diff --git a/wdl/commands/driverbase.py b/wdl/commands/driverbase.py index a2f2210..38835fb 100644 --- a/wdl/commands/driverbase.py +++ b/wdl/commands/driverbase.py @@ -7,6 +7,8 @@ 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""" @@ -19,12 +21,10 @@ def setup_subparser(cls, subparsers: _SubParsersAction, fname_arg_setup: bool=Tr if fname_arg_setup: parser.add_argument("fname", help="file to read input from. Use '-' to read from stdin") - def runner(args: Namespace) -> None: - kwargs = vars(args) - obj: cls = cls(**kwargs) - obj() + def cls_provider(args: Namespace) -> None: + return cls - parser.set_defaults(func=runner) + parser.set_defaults(func=cls_provider) return parser @classmethod @@ -43,8 +43,10 @@ def __init__(self, fname: str, **kwargs): self._text = self._read_file_or_stdin(fname) @abstractmethod - def __call__(self) -> None: ... - + 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 '-'""" diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py index adefdd9..d6c99f8 100644 --- a/wdl/commands/legacy_drivers.py +++ b/wdl/commands/legacy_drivers.py @@ -26,7 +26,7 @@ class SeqParserDriver(WDLDriver): CMD_NAME: str = "seq" CMD_DESCRIPTION: str = "parse a .conf file and make an include list from it" - def __call__(self) -> int: + def __call__(self, cli_mode: bool) -> int: logger.info("making include sequence") make_include_sequence(self._text) return 0 @@ -53,7 +53,7 @@ def __init__(self, fname: str, projname: Optional[str] = None, **kwargs): self._projname: str = projname self._text: str = f.read() - def __call__(self) -> int: + def __call__(self, cli_mode: bool) -> int: logger.debug("writing output to .modules file...") self._write_output("CONFIG", "modules", parse_modules(self._text)) @@ -74,7 +74,7 @@ class IncParserDriver(WDLDriver): CMD_NAME: str = "inc" CMD_DESCRIPTION: str = "parse an include file" - def __call__(self) -> int: + def __call__(self, cli_mode: bool) -> int: make_include(self._text) return 0 @@ -82,7 +82,7 @@ class WdlParserDriver(WDLDriver): CMD_NAME: str = "wdl" CMD_DESCRIPTION: str = "parse the subroutines from a WDL input file" - def __call__(self) -> int: + def __call__(self, cli_mode: bool) -> int: logger.info("parsing WDL file...") # TODO:blergh @@ -113,7 +113,7 @@ def __init__(self, fname: str, plots: bool, **kwargs): self._fname: str = fname self._plots: bool = plots - def __call__(self) -> int: + 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) @@ -144,7 +144,7 @@ def __init__(self, modefile: str, acffile: str, **kwargs): self._modefile = modefile self._acffile = acffile - def __call__(self) -> int: + def __call__(self, cli_mode: bool) -> int: modegen.Modegen(self._modefile, self._acffile) return 0 @@ -162,7 +162,7 @@ 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) -> int: + 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}") diff --git a/wdl/commands/preprocess.py b/wdl/commands/preprocess.py index 200d814..74ef78b 100644 --- a/wdl/commands/preprocess.py +++ b/wdl/commands/preprocess.py @@ -4,19 +4,28 @@ 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 = which("gpp") + self._gpppath = _find_gpp_program() def __call__(self) -> int: if self._gpppath is not None: @@ -26,3 +35,27 @@ def __call__(self) -> int: 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 From 2ae7e89c73ceed635dddef20a1839d986b212070 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 16 Jun 2025 11:47:17 -0700 Subject: [PATCH 17/34] fix issue in ini2acf where some sections were not emitted --- wdl/ini2acf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index bd5039f..b8896cc 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -21,6 +21,7 @@ def _section_replace_filter(inp: str) -> Generator[tuple[str, list[str]], 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: @@ -28,12 +29,13 @@ def _section_replace_filter(inp: str) -> Generator[tuple[str, list[str]], None, # 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, process - thissection = secname + 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: From 96dff5a4078f680f238d898a9bccfed0e0d9ed10 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Sun, 3 Aug 2025 17:57:32 -0700 Subject: [PATCH 18/34] add missing config tag in generated acf --- wdl/ini2acf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index b8896cc..bd9d99d 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -57,8 +57,11 @@ def generate_acf(inifile: str | Path | TextIOWrapper, #This is an already open file like opject inp: str = inifile.read() outp: list[str] = [] + + #need to add [CONFIG] at the top + outp.append("[CONFIG]") + for secname, seclines, process in _section_replace_filter(inp): - outp.append(f"[{secname}]") if process: for ind, line in enumerate(seclines): #strip whitespace and remove any trailing comment @@ -68,6 +71,7 @@ def generate_acf(inifile: str | Path | TextIOWrapper, # 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}" From 8046b97acaea53fcde855978d8e284635017ae4d Mon Sep 17 00:00:00 2001 From: Don Neill Date: Thu, 13 Mar 2025 10:14:28 -0700 Subject: [PATCH 19/34] Handle special sequence "Start" that does not require an exit. (#26) * Handle special sequence "Start" that does not require an exit. * Added comment about special Start sequence --- wdl/wdlParser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index b20014c..09365b8 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -2271,7 +2271,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, } From 600849fca1f7b01ce5ab7d2ad28400feb7c1c797 Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Fri, 1 Aug 2025 10:43:23 -0700 Subject: [PATCH 20/34] Allow floating point values --- wdl/wavgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wdl/wavgen.py b/wdl/wavgen.py index e1e58cf..e4f3ee1 100644 --- a/wdl/wavgen.py +++ b/wdl/wavgen.py @@ -134,10 +134,10 @@ def loadWDL(infile, outfile="/dev/null", verbose=1): 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 From 14297825fa97ccbac2a4533737991c7cd67799e3 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 1 Aug 2025 15:22:53 -0700 Subject: [PATCH 21/34] allow float constants All constants will be floats --- wdl/wavgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wdl/wavgen.py b/wdl/wavgen.py index e4f3ee1..c7d81a6 100644 --- a/wdl/wavgen.py +++ b/wdl/wavgen.py @@ -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 From 63edd7e6f333c9d7f1970b9b30b36aa0151f2512 Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Tue, 16 Sep 2025 16:32:24 -0300 Subject: [PATCH 22/34] Ability to use contants for waveforms and slots --- wdl/wdlParser.py | 236 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 189 insertions(+), 47 deletions(-) diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index 09365b8..e5809b2 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -556,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(";"): @@ -571,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" @@ -600,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 @@ -615,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 @@ -675,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("[") @@ -686,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): @@ -731,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 @@ -749,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): @@ -808,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("[") @@ -826,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): @@ -886,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 @@ -905,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 @@ -920,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): @@ -1018,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(";") @@ -1083,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): @@ -1172,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): @@ -1196,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): @@ -1207,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 @@ -1367,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) + consume(NUMBER) + elif found(IDENTIFIER): + enable = token.cargo + consume(IDENTIFIER) else: enable = "1" @@ -1387,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 @@ -1439,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" @@ -1459,7 +1571,10 @@ 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(",") @@ -1510,7 +1625,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 @@ -1526,7 +1644,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 @@ -1536,13 +1657,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): @@ -1586,8 +1713,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 @@ -1599,7 +1729,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 @@ -1609,13 +1742,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 = "" @@ -1662,7 +1801,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): @@ -1929,7 +2068,10 @@ def to(): if found(NUMBER): # multiply the value by the sign from above (hehe) setLevel = str(sign * float(token.cargo)) - consume(NUMBER) + consume(NUMBER) + elif found(IDENTIFIER): + setLevel = token.cargo + consume(IDENTIFIER) # ----------------------------------------------------------------------------- From 2791ac69dbcec0eb30359261fc8551c7ccff17ea Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Tue, 23 Sep 2025 18:12:27 -0700 Subject: [PATCH 23/34] fix xvbias --- wdl/wdlParser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index e5809b2..f0c9fcb 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -1470,7 +1470,7 @@ def pbias(slot_number): enable = token.cargo if int(enable) < 0 or int(enable) > 1: error("PBIAS enable " + dq(enable) + " must be 0 or 1") - consume(NUMBER) + consume(NUMBER) elif found(IDENTIFIER): enable = token.cargo consume(IDENTIFIER) From 781500f57cbb05f9cf97d30b7d27351c0b39a00f Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 1 Dec 2025 14:06:42 -0800 Subject: [PATCH 24/34] fix modegen driver to actually append the modes --- wdl/commands/legacy_drivers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py index d6c99f8..2a335b5 100644 --- a/wdl/commands/legacy_drivers.py +++ b/wdl/commands/legacy_drivers.py @@ -144,8 +144,9 @@ def __init__(self, modefile: str, acffile: str, **kwargs): self._modefile = modefile self._acffile = acffile - def __call__(self, cli_mode: bool) -> int: - modegen.Modegen(self._modefile, self._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): From 29fd2be3242aa97881ec63e019f5f4bcc466e884 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 1 Dec 2025 14:26:48 -0800 Subject: [PATCH 25/34] fix regex for matching taplines --- wdl/modegen.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wdl/modegen.py b/wdl/modegen.py index ba8629a..509fb43 100644 --- a/wdl/modegen.py +++ b/wdl/modegen.py @@ -59,6 +59,7 @@ def __read_inputfile(self): # look for key=value pairs, matching the LAST = on the line match = re.search("^(.+:.+)\s*=\s*(.+?)\n", line) if match: + print(f"found match: {match}") # union will hold one of every key specified self.union.update({match.group(1): None}) # modes only hold specified key=value pairs @@ -67,8 +68,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('ACF:TAPLINE\d+=\"[\w,-]+\"', line): + print("found tapline") if thismode not in self.taplines: + print("updating tapline!") self.taplines.update({thismode: 1}) else: self.taplines[thismode] += 1 From c3471c97d65620b644ef372fa6fc4e9c59ed9124 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 26 Jan 2026 12:44:19 -0800 Subject: [PATCH 26/34] silence a bunch of errors, fix incorrect module numbering --- wdl/modegen.py | 5 +++-- wdl/wdlParser.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/wdl/modegen.py b/wdl/modegen.py index 509fb43..97a68b7 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) @@ -59,7 +60,7 @@ def __read_inputfile(self): # look for key=value pairs, matching the LAST = on the line match = re.search("^(.+:.+)\s*=\s*(.+?)\n", line) if match: - print(f"found match: {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 @@ -69,7 +70,7 @@ 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): - print("found tapline") + print(f"found tapline {match.group(2)}") if thismode not in self.taplines: print("updating tapline!") self.taplines.update({thismode: 1}) diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index f0c9fcb..a59980b 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -232,7 +232,7 @@ def module(): "ADF": 13, "ADX": 14, "ADLN": 15, - "DRIVERX": 16, + "DRIVERX": 16 } mod_type = module_map.get(module_name.upper(), -1) From 288e0984114a8046181f6a911fb0d4c3060929aa Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 26 Jan 2026 12:48:19 -0800 Subject: [PATCH 27/34] remove warnings about invalid escape sequences by making regex search defs raw strings --- wdl/modegen.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/wdl/modegen.py b/wdl/modegen.py index 97a68b7..5418fba 100644 --- a/wdl/modegen.py +++ b/wdl/modegen.py @@ -49,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): {}}) @@ -58,7 +58,7 @@ 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 @@ -69,7 +69,7 @@ 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!") @@ -83,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) @@ -102,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) @@ -122,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: @@ -155,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=" @@ -165,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: From fd0928991d71e6d6b72d9343ce42796919225648 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 26 Jan 2026 13:25:23 -0800 Subject: [PATCH 28/34] set packages to properly include the library when using standalone build --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 44571e5..3dde280 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ 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" From b3294f6a51431c194d35d5f07fbf21b11471f710 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 26 Jan 2026 13:45:50 -0800 Subject: [PATCH 29/34] fix more warnings about invalid regex strings, fix invalid conversion from 1D array to scalar in recent numpy --- wdl/wavgen.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/wdl/wavgen.py b/wdl/wavgen.py index c7d81a6..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,18 +116,18 @@ 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)) @@ -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") From 2ad64966fe8d91db916b0ba5805e79d72efea043 Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Wed, 28 Jan 2026 14:48:18 -0800 Subject: [PATCH 30/34] fix missing ENABLE statement for the first clock driver definition --- wdl/commands/legacy_drivers.py | 4 ++-- wdl/ini2acf.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/wdl/commands/legacy_drivers.py b/wdl/commands/legacy_drivers.py index 2a335b5..6c870cd 100644 --- a/wdl/commands/legacy_drivers.py +++ b/wdl/commands/legacy_drivers.py @@ -3,7 +3,7 @@ from typing import Optional from argparse import ArgumentParser from sys import stdout - +import os from .driverbase import WDLDriver from wdl.ini2acf import generate_acf @@ -67,7 +67,7 @@ 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}]"]) + f.writelines([f"[{archonkw}]{os.linesep}"]) f.write(output) class IncParserDriver(WDLDriver): diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index bd9d99d..4ab833f 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -59,7 +59,7 @@ def generate_acf(inifile: str | Path | TextIOWrapper, outp: list[str] = [] #need to add [CONFIG] at the top - outp.append("[CONFIG]") + outp.append(f"[CONFIG]{linesep}") for secname, seclines, process in _section_replace_filter(inp): if process: From d6e99bc9d29aa9a9c171d23657c7b6ff409d17ec Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 27 Feb 2026 15:28:51 -0800 Subject: [PATCH 31/34] add -ve numbers to Number parsing, and fixes constants generation. DOES NOT WORK, silently fails to emit code when using constants in a SET statement --- wdl/Symbols.py | 2 +- wdl/wdlParser.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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/wdlParser.py b/wdl/wdlParser.py index a59980b..c18b47c 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -1579,7 +1579,6 @@ def nbias(slot_number): consume(",") # must be a negative number... - consume("-") if found(NUMBER): cmd = token.cargo consume(NUMBER) From 244fe0e9ee5bb69124d1478e27fb014c1fe7353a Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Fri, 27 Feb 2026 15:41:12 -0800 Subject: [PATCH 32/34] improve negative number parsing. Still not generated properly for using constants --- wdl/wdlParser.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index c18b47c..5761324 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -1581,6 +1581,9 @@ def nbias(slot_number): # must be a negative number... if found(NUMBER): cmd = token.cargo + + if float(cmd) > 0.0: + error("XVN value must be negative!") consume(NUMBER) consume("]") @@ -2057,16 +2060,8 @@ 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)) + setLevel = float(token.cargo) consume(NUMBER) elif found(IDENTIFIER): setLevel = token.cargo From 7cbc539a5f2e72407bb3451b319290c63deb92ae Mon Sep 17 00:00:00 2001 From: Dan Weatherill Date: Mon, 2 Mar 2026 16:44:57 -0800 Subject: [PATCH 33/34] remove extra emitted - sign from XVBias generation --- wdl/wdlParser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wdl/wdlParser.py b/wdl/wdlParser.py index 5761324..3e2c8d7 100644 --- a/wdl/wdlParser.py +++ b/wdl/wdlParser.py @@ -1595,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 != "": From df425c3877a6752a0bca8331b28f9fb91858d247 Mon Sep 17 00:00:00 2001 From: meiyu Date: Mon, 29 Jun 2026 17:48:48 -0700 Subject: [PATCH 34/34] Minor bug fix and clean up --- pyproject.toml | 10 ++-------- wdl/ini2acf.py | 9 +++++++-- wdl/modParserDriver.py | 21 ++++++++++++++++----- wdl/modegenDriver.py | 25 ++++++++++++++++++------- wdl/seqParserDriver.py | 23 ++++++++++++++++++----- wdl/wdlParserDriver.py | 23 ++++++++++++++++++----- 6 files changed, 79 insertions(+), 32 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3dde280..59503ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,23 +21,17 @@ dependencies = [ "PyQt5>=5.15" ] - -#note this is a GUESS. With agreement from COO software people (I need context for this) -#I would like to bump this sooner. Note python 3.8 was released in 2019, and is the default python for e.g. ubuntu 20.04 -#On ubuntu 20.04, python 3.10 is available from "official" sources and as recent as python 3.12 is available from PPAs. -#This is not including, of course, installing a conda distribution to get python whatever. -requires-python = ">=3.8" +requires-python = ">=3.12" [project.urls] Repository = "https://github.com/CaltechOpticalObservatories/wdl" [project.scripts] -# the "new" command line tool wdl = "wdl.cli:main" ini2acf = "wdl.ini2acf:main" -#command script wrappers for legacy .py "Driver" scripts +# Legacy command wrappers for historical *.py driver scripts "LexerDriver.py" = "wdl.LexerDriver:__main__" diff --git a/wdl/ini2acf.py b/wdl/ini2acf.py index 4ab833f..b872eea 100644 --- a/wdl/ini2acf.py +++ b/wdl/ini2acf.py @@ -98,11 +98,16 @@ def main(): """) 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) + 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.inifile) + out_content: str = generate_acf(args.infile) if args.outfile is not None: with open(args.outfile,"w") as f: diff --git a/wdl/modParserDriver.py b/wdl/modParserDriver.py index 7bcf0ca..289ee43 100755 --- a/wdl/modParserDriver.py +++ b/wdl/modParserDriver.py @@ -28,12 +28,23 @@ import sys #hack to allow both modern style import and direct script execution -if __package__ != "wdl": - import os +if __package__ in (None, ""): + import sys 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 + 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 diff --git a/wdl/modegenDriver.py b/wdl/modegenDriver.py index f389099..e75796b 100755 --- a/wdl/modegenDriver.py +++ b/wdl/modegenDriver.py @@ -33,14 +33,25 @@ # import fileinput -#hack to allow both modern style import and direct script execution -if __package__ != "wdl": - import os +# Hack to allow both modern style import and direct script execution +if __package__ in (None, ""): + import sys import warnings - basen = os.path.basename(__file__) - warnings.warn(f"detected running a script directly, consider using python -m wdl.{basen}") - import wavgen - import modegen + 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 diff --git a/wdl/seqParserDriver.py b/wdl/seqParserDriver.py index f1a4cd6..700b327 100755 --- a/wdl/seqParserDriver.py +++ b/wdl/seqParserDriver.py @@ -23,12 +23,25 @@ # Stephen Kaye #hack to allow both modern style import and direct script execution -if __package__ != "wdl": - import os +if __package__ in (None, ""): + import sys 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 + 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 diff --git a/wdl/wdlParserDriver.py b/wdl/wdlParserDriver.py index e92cd70..ecca753 100755 --- a/wdl/wdlParserDriver.py +++ b/wdl/wdlParserDriver.py @@ -24,12 +24,25 @@ #hack to allow both modern style import and direct script execution -if __package__ != "wdl": - import os +if __package__ in (None, ""): + import sys 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 + 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