"""
Variable property mapping (VPM).
"""
import re
from pathlib import Path
from delphin import variable
# Default modules need to import the PyDelphin version
from delphin.__about__ import __version__ # noqa: F401
from delphin.exceptions import PyDelphinSyntaxError
_LR_OPS = set(["<>", ">>", "==", "=>"])
_RL_OPS = set(["<>", "<<", "==", "<="])
# _SUBSUME_OPS = set(['<>', '<<', '>>']) # (usused) # noqa: ERA001
_EQUAL_OPS = set(["==", "<=", "=>"])
_ALL_OPS = _LR_OPS.union(_RL_OPS)
[docs]
class VPMSyntaxError(PyDelphinSyntaxError):
"""Raised when loading an invalid VPM."""
[docs]
def load(source, semi=None):
"""
Read a variable-property mapping from *source* and return the VPM.
Args:
source: a filename or file-like object containing the VPM
definitions
semi (:class:`~delphin.semi.SemI`, optional): if provided,
it is passed to the VPM constructor
Returns:
a :class:`VPM` instance
"""
if hasattr(source, "read"):
return _load(source, semi)
else:
source = Path(source).expanduser()
with source.open("r") as fh:
return _load(fh, semi)
def _load(fh, semi):
filename = getattr(fh, "name", "<stream>")
typemap = []
propmap = []
curmap = typemap
lh, rh = 1, 1 # number of elements expected on each side
for lineno, line in enumerate(fh, 1):
line = line.lstrip()
if not line or line.startswith(";"):
continue
match = re.match(r"(?P<lfeats>[^:]+):(?P<rfeats>.+)", line)
if match is not None:
lfeats = match.group("lfeats").split()
rfeats = match.group("rfeats").split()
lh, rh = len(lfeats), len(rfeats)
curmap = []
propmap.append(((lfeats, rfeats), curmap))
continue
match = re.match(r"(?P<lvals>.*)(?P<op>[<>=]{2})(?P<rvals>.*)$", line)
if match is not None:
lvals = match.group("lvals").split()
op = match.group("op")
rvals = match.group("rvals").split()
msg, offset = "", -1
if len(lvals) != lh:
msg = "wrong number of values on left side"
offset = match.end("lvals")
if op not in _ALL_OPS:
msg = "invalid operator"
offset = match.end("op")
elif len(rvals) != rh:
msg = "wrong number of values on right side"
offset = match.end("rvals")
if msg:
raise VPMSyntaxError(
msg, filename=filename, lineno=lineno, offset=offset, text=line
)
curmap.append((lvals, op, rvals))
continue
raise VPMSyntaxError(
"invalid line in VPM file", filename=filename, lineno=lineno, text=line
)
return VPM(typemap, propmap, semi)
[docs]
class VPM:
"""
A variable-property mapping.
This class contains the rules for mapping variable properties from
the grammar-internal definitions to grammar-external ones, and back
again.
Args:
typemap: an iterable of (src, OP, tgt) iterables
propmap: an iterable of (featset, valmap) tuples, where
featmap is a tuple of two lists: (source_features,
target_features); and valmap is a list of value tuples:
(source_values, OP, target_values)
semi (:class:`~delphin.semi.SemI`, optional): if provided,
this is used for more sophisticated value comparisons
"""
def __init__(self, typemap, propmap, semi=None):
"""
Initialize a new variable-property mapping instance.
"""
self._typemap = typemap # [(src, OP, tgt)]
self._propmap = propmap # [((srcfs, tgtfs), [(srcvs, OP, tgtvs)])]
self._semi = semi
[docs]
def apply(self, var, props, reverse=False):
"""
Apply the VPM to variable *var* and properties *props*.
Args:
var: a variable
props: a dictionary mapping properties to values
reverse: if `True`, apply the rules in reverse (e.g. from
grammar-external to grammar-internal forms)
Returns:
a tuple (v, p) of the mapped variable and properties
"""
vs, vid = variable.split(var)
if reverse:
# variable type mapping is disabled in reverse;
# otherwise this would be (b, op, a) for each (a, op, b) in
# self._typemap if the op is in _RL_OPS
tms = []
else:
tms = [(a, op, b) for a, op, b in self._typemap if op in _LR_OPS]
for src, op, tgt in tms:
if _valmatch([vs], src, op, None, self._semi, "variables"):
vs = vs if tgt == ["*"] else tgt[0]
break
newvar = f"{vs}{vid}"
newprops = {}
for featsets, valmap in self._propmap:
if reverse:
tgtfeats, srcfeats = featsets
pms = [(b, op, a) for a, op, b in valmap if op in _RL_OPS]
else:
srcfeats, tgtfeats = featsets
pms = [(a, op, b) for a, op, b in valmap if op in _LR_OPS]
vals = [props.get(f) for f in srcfeats]
for srcvals, op, tgtvals in pms:
if _valmatch(vals, srcvals, op, vs, self._semi, "properties"):
for i, featval in enumerate(zip(tgtfeats, tgtvals, strict=True)):
k, v = featval
if v == "*":
if i < len(vals) and vals[i] is not None:
newprops[k] = vals[i]
elif v != "!":
newprops[k] = v
break
return newvar, newprops
def _valmatch(vs, ss, op, varsort, semi, section):
"""
Return `True` if for every paired *v* and *s* from *vs* and *ss*:
v <> s (subsumption or equality if *semi* is `None`)
v == s (equality)
s == '*'
s == '!' and v == `None`
s == '[xyz]' and varsort == 'xyz'
"""
if op in _EQUAL_OPS or semi is None:
return all(
s == v # value equality
or (s == "*" and v is not None) # non-null wildcard
or (
v is None
and ( # value is null (any or with matching varsort)
s == "!" or (s[0], s[-1], s[1:-1]) == ("[", "]", varsort)
)
)
for v, s in zip(vs, ss, strict=False)
)
else:
pass