Skip to content

Commit

Permalink
Merge pull request #265 from delph-in/develop
Browse files Browse the repository at this point in the history
v1.1.0 release
  • Loading branch information
goodmami authored Jan 7, 2020
2 parents 6d992c3 + e8ee148 commit 880a389
Show file tree
Hide file tree
Showing 18 changed files with 489 additions and 208 deletions.
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ these changes are prefixed with "**BREAKING**"

(no unreleased changes yet)


## [v1.1.0][]

**Release date: 2020-01-07**

Removes the NetworkX dependency, resolves some TSQL bugs, and adds
some features to Derivations.

### Added

* `requirements.txt` mainly to try and help GitHub detect dependencies
* `delphin.derivation.UDFNode.parent` ([#245][])
* `delphin.derivation.UDFNode.internals()` ([#246][])
* `delphin.derivation.UDFTerminal.parent` ([#245][])

### Changed

* `delphin.mrs.is_isomorphic()` no longer uses NetworkX ([#263][])
* Docs no longer require the `sphinx_autodoc_typehints` package as of
Sphinx 2.2.0 ([#264][])
* `delphin.exceptions.PyDelphinSyntaxError` now puts the ^ marker in
the right spot.
* Lexing in `delphin.util` now tracks the original line with each
token, allowing for more informative syntax errors.
* `delphin.tsql.select()` will raise a `TSQLError` if a condition's
type does not match that of the column it checks ([#261][])
* `delphin.tsql.select()` now considers ``None`` values when checking
conditions ([#262][])


## [v1.0.3][]

**Release date: 2019-11-29**
Expand Down Expand Up @@ -1072,6 +1102,7 @@ information about changes, except for
[commit messages](../../commits/v0.2).

[unreleased]: ../../tree/develop
[v1.1.0]: ../../releases/tag/v1.1.0
[v1.0.3]: ../../releases/tag/v1.0.3
[v1.0.2]: ../../releases/tag/v1.0.2
[v1.0.1]: ../../releases/tag/v1.0.1
Expand Down Expand Up @@ -1186,10 +1217,16 @@ information about changes, except for
[#200]: https://github.com/delph-in/pydelphin/issues/200
[#203]: https://github.com/delph-in/pydelphin/issues/203
[#213]: https://github.com/delph-in/pydelphin/issues/213
[#245]: https://github.com/delph-in/pydelphin/issues/245
[#246]: https://github.com/delph-in/pydelphin/issues/246
[#247]: https://github.com/delph-in/pydelphin/issues/247
[#248]: https://github.com/delph-in/pydelphin/issues/248
[#249]: https://github.com/delph-in/pydelphin/issues/249
[#250]: https://github.com/delph-in/pydelphin/issues/250
[#252]: https://github.com/delph-in/pydelphin/issues/252
[#253]: https://github.com/delph-in/pydelphin/issues/253
[#257]: https://github.com/delph-in/pydelphin/issues/257
[#261]: https://github.com/delph-in/pydelphin/issues/261
[#262]: https://github.com/delph-in/pydelphin/issues/262
[#263]: https://github.com/delph-in/pydelphin/issues/263
[#264]: https://github.com/delph-in/pydelphin/issues/264
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ For bug requests, please provide the following, if possible:
```python
>>> from delphin.__about__ import __version__
>>> __version__ # distribution version
'1.0.0'
'1.1.0'
>>> from delphin import mrs
>>> mrs.__version__ # package version
'1.0.0'
'1.1.0'
```
* Python version (e.g. 3.5, 3.6, etc.)

Expand Down
2 changes: 1 addition & 1 deletion delphin/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# the warehouse project:
# https://github.com/pypa/warehouse/blob/master/warehouse/__about__.py

__version__ = '1.0.3'
__version__ = '1.1.0'
__version_info__ = __version__.replace('.', ' ').replace('-', ' ').split()

__title__ = 'PyDelphin'
Expand Down
19 changes: 19 additions & 0 deletions delphin/derivation.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def __ne__(self, other):
return NotImplemented
return not (self == other)

@property
def parent(self):
return self._parent

# serialization

def to_udf(self, indent=1):
Expand Down Expand Up @@ -330,6 +334,21 @@ def terminals(self):
nodes.extend(dtr.terminals())
return nodes

def internals(self):
"""
Return the list of internal nodes.
Internal nodes are nodes above preterminals. In other words,
the union of internals and preterminals is the set of
nonterminal nodes.
"""
if any(isinstance(dtr, UDFTerminal) for dtr in self.daughters):
return []
nodes = [self]
for dtr in self.daughters:
nodes.extend(dtr.internals())
return nodes


class Derivation(UDFNode):
"""
Expand Down
2 changes: 1 addition & 1 deletion delphin/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __str__(self):
if self.text is not None:
parts.append(' ' + self.text)
if self.offset is not None:
parts.append(' ' + (' ' * self.offset) + '^')
parts.append(' ' + (' ' * self.offset) + '^')
elif parts:
parts[-1] += ', character {}'.format(self.offset)
if self.message is not None:
Expand Down
4 changes: 2 additions & 2 deletions delphin/mrs/_mrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ def __init__(self,
if variables is None:
variables = {}

self.hcons = hcons
self.icons = icons
self.hcons = list(hcons)
self.icons = list(icons)
self.variables = _fill_variables(
variables, top, index, rels, hcons, icons)

Expand Down
48 changes: 30 additions & 18 deletions delphin/mrs/_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Operations on MRS structures
"""

from typing import Iterable, Dict, Set
from typing import Iterable, Dict, Set, Optional

from delphin import variable
from delphin import predicate
Expand Down Expand Up @@ -154,19 +154,25 @@ def is_isomorphic(m1: mrs.MRS,
properties: if `True`, ensure variable properties are
equal for mapped predications
"""
# loading NetworkX is slow; only do this when is_isomorphic is called
import networkx as nx
# simple tests
if (len(m1.rels) != len(m2.rels)
or len(m1.hcons) != len(m2.hcons)
or len(m1.icons) != len(m2.icons)
or len(m1.variables) != len(m2.variables)):
return False

m1dg = _make_mrs_digraph(m1, nx.DiGraph(), properties)
m2dg = _make_mrs_digraph(m2, nx.DiGraph(), properties)
g1 = _make_mrs_isograph(m1, properties)
g2 = _make_mrs_isograph(m2, properties)

def nem(m1d, m2d): # node-edge-match
return m1d.get('sig') == m2d.get('sig')
iso = util._isomorphism(g1, g2, m1.top, m2.top)
return set(iso) == set(g1)

return nx.is_isomorphic(m1dg, m2dg, node_match=nem, edge_match=nem)

def _make_mrs_isograph(x, properties):
g = {} # type: Dict[Identifier, Dict[Optional[Identifier], str]]
g.update((v, {}) for v in x.variables)
g.update((ep.id, {}) for ep in x.rels)

def _make_mrs_digraph(x, dg, properties):
for ep in x.rels:
# optimization: retrieve early to avoid successive lookup
lbl = ep.label
Expand All @@ -175,7 +181,7 @@ def _make_mrs_digraph(x, dg, properties):
args = ep.args
carg = ep.carg
# scope labels (may be targets of arguments or hcons)
dg.add_edge(lbl, id, sig='eq-scope')
g[lbl][id] = 'eq-scope'
# predicate-argument structure
s = predicate.normalize(ep.predicate)
if carg is not None:
Expand All @@ -186,16 +192,22 @@ def _make_mrs_digraph(x, dg, properties):
val = props[prop]
proplist.append('{}={}'.format(prop.upper(), val.lower()))
s += '{' + '|'.join(proplist) + '}'
dg.add_node(id, sig=s)
dg.add_edges_from((id, args[role], {'sig': role})
for role in args if role != mrs.CONSTANT_ROLE)
g[id][None] = s
for role in args:
if role != mrs.CONSTANT_ROLE:
# there may be multiple roles (e.g., L-INDEX, L-HNDL, etc.)
roles = g[id].get(args[role], '').split() + [role]
g[id][args[role]] = ' '.join(sorted(roles))

# hcons
dg.add_edges_from((hc.hi, hc.lo, {'sig': hc.relation})
for hc in x.hcons)
for hc in x.hcons:
g[hc.hi][hc.lo] = hc.relation

# icons
dg.add_edges_from((ic.left, ic.right, {'sig': ic.relation})
for ic in x.icons)
return dg
for ic in x.icons:
g[ic.left][ic.right] = ic.relation

return g


def compare_bags(testbag: Iterable[mrs.MRS],
Expand Down
73 changes: 47 additions & 26 deletions delphin/tsql.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
"""

from typing import (
List, Tuple, Dict, Set, Mapping, Optional, Union, Any, Type,
List, Tuple, Dict, Set, Optional, Union, Any, Type,
Iterator, Callable, cast as typing_cast)
import operator
import re
from datetime import datetime

from delphin.exceptions import PyDelphinException, PyDelphinSyntaxError
from delphin import util
Expand Down Expand Up @@ -39,6 +40,8 @@ class TSQLSyntaxError(PyDelphinSyntaxError):
_Condition = Union[_Comparison, _Boolean]
_FilterFunction = Callable[[tsdb.Record], bool]

_QNameResolver = Callable[[str], Tuple[str, tsdb.Field]]


class _Record(tsdb.Record):
"""Dummy Record type to mimic the call signature of itsdb.Row."""
Expand Down Expand Up @@ -196,13 +199,12 @@ def _make_execution_plan(
condition: Optional[_Condition],
db: tsdb.Database) -> Tuple:
"""Make a plan for all relations to join and columns to keep."""
schema_map = _make_schema_map(db, relations)
resolve_qname = _make_qname_resolver(schema_map)
resolve_qname = _make_qname_resolver(db, relations)

if projection == ['*']:
projection = _project_all(relations, db)
else:
projection = [resolve_qname(name) for name in projection]
projection = [resolve_qname(name)[0] for name in projection]

cond_resolved = None # type: Optional[_Condition]
cond_fields = [] # type: _Names
Expand Down Expand Up @@ -230,10 +232,16 @@ def _project_all(relations: List[str], db: tsdb.Database) -> List[str]:
return projection


def _make_schema_map(
def _make_qname_resolver(
db: tsdb.Database,
relations: List[str]) -> Mapping[str, List[str]]:
"""Return an inverse mapping from field names to relations."""
relations: List[str]) -> _QNameResolver:
"""
Return a function that turns column names into qualified names.
For example, `i-input` becomes `item.i-input`.
"""

index = {rel: tsdb.make_field_index(db.schema[rel]) for rel in db.schema}
schema_map = {} # type: Dict[str, List[str]]
for relname, fields in db.schema.items():
for field in fields:
Expand All @@ -243,25 +251,17 @@ def _make_schema_map(
schema_map[colname] = sorted(schema_map[colname],
key=relations.__contains__,
reverse=True)
return schema_map


def _make_qname_resolver(schema_map):
"""
Return a function that turns column names into qualified names.
For example, `i-input` becomes `item.i-input`.
"""

def resolve(colname: str) -> str:
def resolve(colname: str) -> Tuple[str, tsdb.Field]:
rel, _, col = colname.rpartition('.')
if rel:
qname = colname
elif col in schema_map:
qname = '{}.{}'.format(schema_map[col][0], col)
rel = schema_map[col][0]
qname = '{}.{}'.format(rel, col)
else:
raise TSQLError('undefined column: {}'.format(colname))
return qname
return qname, db.schema[rel][index[rel][col]]

return resolve

Expand Down Expand Up @@ -365,7 +365,7 @@ def add_edges(keys):

def _process_condition_fields(
condition: _Condition,
resolve_qname: Callable[[str], str]) -> Tuple[_Condition, _Names]:
resolve_qname: _QNameResolver) -> Tuple[_Condition, _Names]:
# conditions are something like:
# ('==', ('i-id', 11))
op, body = condition
Expand All @@ -374,20 +374,41 @@ def _process_condition_fields(
fieldset = set()
conditions = []
for cond in body:
_cond, _fields = _process_condition_fields(cond, resolve_qname)
_cond, _fields = _process_condition_fields(
cond, resolve_qname)
fieldset.update(_fields)
conditions.append(_cond)
return (op, conditions), sorted(fieldset)

elif op == 'not':
ncond, fields = _process_condition_fields(body, resolve_qname)
ncond, fields = _process_condition_fields(
body, resolve_qname)
return ('not', ncond), fields

else:
qname = resolve_qname(body[0])
qname, field = resolve_qname(body[0])

# check if the condition's type matches the column
typ = _expected_type(field.datatype)
if not isinstance(body[1], typ):
raise TSQLError(
'type mismatch in condition on {}: {} {} {}'
.format(qname, typ.__name__, op, type(body[1]).__name__))

return (op, (qname, body[1])), [qname]


def _expected_type(datatype):
if datatype == ':string':
return str
elif datatype in ':integer':
return int
elif datatype in ':float':
return (int, float)
elif datatype in ':date':
return datetime


def _process_condition_function(
condition: _Condition,
selection: Selection) -> _FilterFunction:
Expand Down Expand Up @@ -419,15 +440,15 @@ def func(row):
index = field_index[body[0]]
field = fields[index]
value = tsdb.cast(field.datatype, row[index])
return re.search(body[1], value)
return value is not None and re.search(body[1], value)

elif op == '!~':

def func(row):
index = field_index[body[0]]
field = fields[index]
value = tsdb.cast(field.datatype, row[index])
return not re.search(body[1], value)
return value is None or not re.search(body[1], value)

else:
compare = _operator_functions[op]
Expand All @@ -436,7 +457,7 @@ def func(row):
index = field_index[body[0]]
field = fields[index]
value = tsdb.cast(field.datatype, row[index])
return compare(value, body[1])
return value is not None and compare(value, body[1])

return func

Expand Down
Loading

0 comments on commit 880a389

Please sign in to comment.