Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0dc65a9
start on a unified CLI callable for WDL
weatherhead99 Feb 28, 2025
7b34d3a
clarify a comment
weatherhead99 Feb 28, 2025
4799da0
initial attempt at a replacement for ini2acf.pl
weatherhead99 Feb 28, 2025
3a11426
script installs and finish initial ini2acf replacement
weatherhead99 Mar 6, 2025
8220c16
add minimum required python
weatherhead99 Mar 6, 2025
c841ef1
allow executing wdl directly as a module
weatherhead99 Mar 7, 2025
7653bb9
add ini2acf subcommand to new CLI app
weatherhead99 Mar 7, 2025
694ccf8
fix (very bad) typo in pyproject.toml definition
weatherhead99 Mar 7, 2025
07387dd
glob is not a function in pathlib, but a function of Path
weatherhead99 Mar 7, 2025
1f6b5b5
refactor CLI command code
weatherhead99 Mar 7, 2025
27d08ae
start on preprocessor utility commands
weatherhead99 Mar 7, 2025
11d8817
more minor driver CLI fixes
weatherhead99 Mar 10, 2025
c6ddfde
add import hacks to all legacy drivers so they can be directly run in…
weatherhead99 Jun 6, 2025
a8088e5
finish ini2acf script rewrite
weatherhead99 Jun 6, 2025
059e7b0
remove spurious extra prints.
weatherhead99 Jun 6, 2025
b1bda95
work on building in gpp preprocessor
weatherhead99 Jun 6, 2025
2ae7e89
fix issue in ini2acf where some sections were not emitted
weatherhead99 Jun 16, 2025
96dff5a
add missing config tag in generated acf
Aug 4, 2025
8046b97
Handle special sequence "Start" that does not require an exit. (#26)
scizen9 Mar 13, 2025
600849f
Allow floating point values
prkrtg Aug 1, 2025
1429782
allow float constants
astronomerdave Aug 1, 2025
63edd7e
Ability to use contants for waveforms and slots
prkrtg Sep 16, 2025
2791ac6
fix xvbias
prkrtg Sep 24, 2025
781500f
fix modegen driver to actually append the modes
Dec 1, 2025
29fd2be
fix regex for matching taplines
Dec 1, 2025
c3471c9
silence a bunch of errors, fix incorrect module numbering
Jan 26, 2026
288e098
remove warnings about invalid escape sequences by making regex search…
Jan 26, 2026
fd09289
set packages to properly include the library when using standalone build
Jan 26, 2026
b3294f6
fix more warnings about invalid regex strings, fix invalid conversion…
Jan 26, 2026
2ad6496
fix missing ENABLE statement for the first clock driver definition
Jan 28, 2026
d6e99bc
add -ve numbers to Number parsing, and fixes constants generation. DO…
Feb 27, 2026
244fe0e
improve negative number parsing. Still not generated properly for usi…
Feb 27, 2026
7cbc539
remove extra emitted - sign from XVBias generation
Mar 3, 2026
df425c3
Minor bug fix and clean up
prkrtg Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ requires = ["setuptools>=64", "wheel", "setuptools_scm>=8"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
py-modules = ["wdl"]
packages = ["wdl"]

[tool.setuptools_scm]
version_file = "wdl/_version.py"

[project]
name = "wdl"
dyanmic = ["version"]
dynamic = ["version"]

description = "WDL (Waveform Definition Language) is designed for use with Archons. This library provides tools and instructions for setting up and using WDL efficiently."
authors = [{name = "Caltech Optical Observatories"}]
Expand All @@ -20,6 +20,18 @@ dependencies = [
"matplotlib>=3.9.2",
"PyQt5>=5.15"
]

requires-python = ">=3.12"

[project.urls]
Repository = "https://github.com/CaltechOpticalObservatories/wdl"


[project.scripts]
wdl = "wdl.cli:main"
ini2acf = "wdl.ini2acf:main"

# Legacy command wrappers for historical *.py driver scripts
"LexerDriver.py" = "wdl.LexerDriver:__main__"


19 changes: 15 additions & 4 deletions wdl/Lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,21 @@
# David Hale <dhale@caltech.edu> or
# Stephen Kaye <skaye@caltech.edu>

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 = ""
Expand Down
4 changes: 2 additions & 2 deletions wdl/LexerDriver.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this likely needs the same hack as Lexer.py and seqParserDriver.py etc,

old WDL makefiles assume you can call these directly, and also assume that they're sat directly next to top level imports of other WDL stuff. The "new" cli I wrote doesn't. So we need to support both to prevent breakage of old WDL code. I think I didn't find any instance where LexerDriver.py was actually run directly in a Makefile context but I could be wrong about that.

Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
# David Hale <dhale@caltech.edu> or
# Stephen Kaye <skaye@caltech.edu>

import Lexer
from Symbols import EOF
from . import Lexer
from .Symbols import EOF
import fileinput


Expand Down
2 changes: 1 addition & 1 deletion wdl/Symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "'" + '"'
Expand Down
4 changes: 4 additions & 0 deletions wdl/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from wdl.cli import main

if __name__ == "__main__":
main()
58 changes: 58 additions & 0 deletions wdl/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from argparse import ArgumentParser
import sys
from .commands.driverbase import WDLDriver
from .commands.legacy_drivers import (
SeqParserDriver, ModParserDriver, IncParserDriver,
WdlParserDriver, WavgenDriver, ModegenDriver, Ini2acfDriver)

from .commands.preprocess import FindGPP, PreprocessGPP

import logging

logger = logging.getLogger(__name__)

def main():
ap = ArgumentParser(prog="wdl",
description="command line interface to Waveform Definition Language (WDL)")
ap.add_argument("--debug", help="print verbose debugging output", action="store_true")
subparsers = ap.add_subparsers(required=True, help="the WDL subcommand to run",
dest="command_name")

#NOTE: could have done a fancy autoreg thing here but there's only a few and this is likely
#clearer until there are lots more IMO
command_classes: list[type] = [SeqParserDriver, ModParserDriver, IncParserDriver, WdlParserDriver,
WavgenDriver, ModegenDriver, Ini2acfDriver, FindGPP, PreprocessGPP]

for cls in command_classes:
cls.setup_subparser(subparsers)

args, unknown_args = ap.parse_known_args()


if args.debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)

#the default func is setup by the class method above to just return which class it is
#that will be operating the command
run_command_tp: type[WDLDriver] = args.func(args)

if run_command_tp.ACCEPTS_EXTRA_ARGS:
obj = run_command_tp(unknown_args, **vars(args))
elif len(unknown_args) > 0:
logger.error("extra (unknown) arguments provided to command that doesn't accept them")
logger.error(f"those arguments were: {unknown_args}")
#my instinct is to throw here but that seems unpopular round these parts so just bail out
sys.exit(1)
else:
kwargs = vars(args)
obj = run_command_tp(**kwargs)

#run the command
return obj(cli_mode=True)


if __name__ == "__main__":
retcode = main()
sys.exit(retcode)
Empty file added wdl/commands/__init__.py
Empty file.
70 changes: 70 additions & 0 deletions wdl/commands/driverbase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from abc import ABCMeta, abstractmethod
from argparse import ArgumentParser, _SubParsersAction, Namespace
from pathlib import Path
from io import TextIOWrapper
from sys import stdin
from contextlib import contextmanager
from typing import ContextManager

class WDLDriver(metaclass=ABCMeta):
ACCEPTS_EXTRA_ARGS: bool = False

@classmethod
def setup_subparser(cls, subparsers: _SubParsersAction, fname_arg_setup: bool=True) -> ArgumentParser:
"""sets up a sub-parser for arguments specific to this particular type of WDL subcommand driver"""

#Get a name to put in the parser, either explicit or named after the class
cmdname: str = getattr(cls, "CMD_NAME", cls.__name__)
cmddesc: str = getattr(cls, "CMD_DESCRIPTION", None)
parser = subparsers.add_parser(name=cmdname, description=cmddesc)

if fname_arg_setup:
parser.add_argument("fname", help="file to read input from. Use '-' to read from stdin")

def cls_provider(args: Namespace) -> None:
return cls

parser.set_defaults(func=cls_provider)
return parser

@classmethod
def find_input_files(cls, basepath: str | Path) -> list[str]:
if not hasattr(cls, "CMD_FILE_EXTENSIONS"):
raise AttributeError("don't know file extensions to search for")
if isinstance(basepath, str):
basepath: Path = Path(basepath)
for extn in cls.CMD_FILE_EXTENSIONS:
possible_files = basepath.glob(f"*.{extn}")
return possible_files

def __init__(self, fname: str, **kwargs):
"""The default __init__ of a driver takes a filename and reads it into the ._text attribute of the class.
Since this is the most common operation of a WDL driver program"""
self._text = self._read_file_or_stdin(fname)

@abstractmethod
def __call__(self, cli_mode: bool) -> None:
"""This method will be called when the driver runs from the command line interface"""
pass

@contextmanager
def _file_or_stdin(self, fname: str) -> ContextManager[TextIOWrapper]:
"""context manager that opens a specified file or uses sys.stdin for the special case of file '-'"""
if fname == "-":
yield stdin
else:
f = open(fname, "r")
yield f
f.close()

@contextmanager
def _file_or_stdout(self, output: TextIOWrapper | str) -> ContextManager[TextIOWrapper]:
if isinstance(output, str):
f = open(output, "w")
yield f
else:
yield output

def _read_file_or_stdin(self, fname: str) -> str:
with self._file_or_stdin(fname) as f:
return f.read()
171 changes: 171 additions & 0 deletions wdl/commands/legacy_drivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import logging
import warnings
from typing import Optional
from argparse import ArgumentParser
from sys import stdout
import os
from .driverbase import WDLDriver

from wdl.ini2acf import generate_acf


# horrible legacy code imports here, TODO: gradually chip away at it
import wdl.wavgen as wavgen
import wdl.modegen as modegen
from wdl.wdlParser import make_include_sequence, parse_modules, parse_system, make_include
import wdl.wdlParser as Parser

# TODO: separate out plotting, no need for this
import matplotlib.pyplot as plt


logger = logging.getLogger(__name__)


class SeqParserDriver(WDLDriver):
CMD_NAME: str = "seq"
CMD_DESCRIPTION: str = "parse a .conf file and make an include list from it"

def __call__(self, cli_mode: bool) -> int:
logger.info("making include sequence")
make_include_sequence(self._text)
return 0


class ModParserDriver(WDLDriver):
CMD_NAME: str = "mod"
CMD_DESCRIPTION: str = "parse a .mod file and create .system and .modules from it"

@classmethod
def setup_subparser(cls, *args, **kwargs) -> ArgumentParser:
parser = super().setup_subparser(*args, **kwargs)
parser.add_argument("--projname", type=str, help="""project name to use. If not given,
the legacy behaviour is followed (the first line of the input text is assumed to be the project name""")
return parser

def __init__(self, fname: str, projname: Optional[str] = None, **kwargs):
with self._file_or_stdin(fname) as f:
if projname is None:
warnings.warn("parsing module file assuming the first line is the project name... please use the --name argument to the command instead")
projname = f.readline().strip()

logger.info("project name is: %s", projname)
self._projname: str = projname
self._text: str = f.read()

def __call__(self, cli_mode: bool) -> int:
logger.debug("writing output to .modules file...")
self._write_output("CONFIG", "modules", parse_modules(self._text))

#Presumably the reason parse_system() here needs no keywords is some hideous global state
# awfulness. DO NOT MOVE THIS LINE ABOVE THE PREVIOUS ONE, therefore
logger.debug("writing output to .system file...")
self._write_output("SYSTEM", "system", parse_system())
return 0

def _write_output(self, archonkw: str, fileext: str, output: str) -> int:
fname: str = f"{self._projname}.{fileext}"
logger.debug("filename is: %s", fname)
with open(fname, "w") as f:
f.writelines([f"[{archonkw}]{os.linesep}"])
f.write(output)

class IncParserDriver(WDLDriver):
CMD_NAME: str = "inc"
CMD_DESCRIPTION: str = "parse an include file"

def __call__(self, cli_mode: bool) -> int:
make_include(self._text)
return 0

class WdlParserDriver(WDLDriver):
CMD_NAME: str = "wdl"
CMD_DESCRIPTION: str = "parse the subroutines from a WDL input file"

def __call__(self, cli_mode: bool) -> int:
logger.info("parsing WDL file...")

# TODO:blergh
global subroutines
subroutines = Parser.get_subroutines(self._text)
Parser.get_params(self._text)
output: str = Parser.parse(self._text)

#apparently this one just prints it out, to stdout I guess?
stdout.write(output)
return 0

class WavgenDriver(WDLDriver):
CMD_NAME: str = "wavgen"
CMD_DESCRIPTION: str = "generate archon waveforms from WDL description"

@classmethod
def setup_subparser(cls, *args, **kwargs) -> ArgumentParser:
parser = super().setup_subparser(*args, **kwargs)
parser.add_argument("--plots", action="store_true", help="generate plots to go with waveforms")
return parser

def __init__(self, fname: str, plots: bool, **kwargs):
#For some reason I don't understand, legacy wavegenDriver.py wants the BASE NAME of the project as
#the argument, not the actual filename. Implement this behaviour here for compatibility even though
#I must stress it seems a little odd

self._fname: str = fname
self._plots: bool = plots

def __call__(self, cli_mode: bool) -> int:
# global variable because OF COURSE IT IS
wavgen.GenerateFigs = self._plots
wavgen.loadWDL(f"{self._fname}.wdl", self._fname)

# ok this breaks previous behaviour because even if plot option was false
# the original program still did a plt.show(). This seems utterly without reason to me
# so here we only actually do that if it was asked for...
# Again, all the plots are done with implicit global state rather than using the MPL object interface...
if self._plots:
plt.show(block=True)

return 0


class ModegenDriver(WDLDriver):
CMD_NAME: str = "modegen"
CMD_DESCRIPTION: str = "generate archon modes from WDL description"

@classmethod
def setup_subparser(cls, subparsers) -> ArgumentParser:
parser = super().setup_subparser(subparsers, fname_arg_setup=False)
#need two filenames here, a mode file and an acf file
parser.add_argument("modefile", type=str, help="the mode file to use")
parser.add_argument("acffile", type=str, help="the ACF file to append to")
return parser

def __init__(self, modefile: str, acffile: str, **kwargs):
self._modefile = modefile
self._acffile = acffile

def __call__(self, cli_mode: bool, append: bool=True) -> int:
mobj = modegen.Modegen(self._modefile, self._acffile)
mobj.write(append)
return 0

class Ini2acfDriver(WDLDriver):
CMD_NAME: str = "ini2acf"
CMD_DESCRIPTION: str = "convert a INI syntax file into ACF"

@classmethod
def setup_subparser(cls, subparsers) -> ArgumentParser:
parser = super().setup_subparser(subparsers)
parser.add_argument("-o,--outfile",help="output file to use. By default, outputs to stdout", type=str)
return parser

def __init__(self, fname: str, outfile: Optional[str]=None, **kwargs):
super().__init__(fname)
self._outfile = stdout if outfile is None else outfile

def __call__(self, cli_mode: bool) -> int:
outtxt: str = generate_acf(self._text, treat_str_as_content=True)
with self._file_or_stdout(self._outfile) as f:
logger.debug(f"outtxt is: {outtxt}")
f.write(outtxt)
return 0
Loading
Loading