Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for parsing dotted names into tags #57

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
python-version: '3.x'
- name: "Main Script"
run: |
EXTRA_INSTALL="pymbolic"
EXTRA_INSTALL="pymbolic lark-parser"
curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/prepare-and-run-pylint.sh
. ./prepare-and-run-pylint.sh "$(basename $GITHUB_REPOSITORY)" test/test_*.py

Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:
# AK, 2020-12-13
rm pytools/log.py

EXTRA_INSTALL="numpy"
EXTRA_INSTALL="numpy lark-parser"
curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh
. ./build-and-test-py-project.sh

Expand Down Expand Up @@ -106,7 +106,7 @@ jobs:
python-version: '3.x'
- name: "Main Script"
run: |
EXTRA_INSTALL="numpy"
EXTRA_INSTALL="numpy lark-parser"
curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/ci-support.sh
. ci-support.sh
build_py_project_in_venv
Expand Down
6 changes: 3 additions & 3 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Pytest:
# AK, 2020-12-13
rm pytools/log.py

export EXTRA_INSTALL="numpy"
export EXTRA_INSTALL="numpy lark-parser"
Copy link
Owner

Choose a reason for hiding this comment

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

Could you push a sister MR to Gitlab (add the link to the PR description) to ensure that passes, too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh
. ./build-and-test-py-project.sh
tags:
Expand Down Expand Up @@ -50,7 +50,7 @@ Mypy:

Pylint:
script:
- EXTRA_INSTALL="pymbolic"
- EXTRA_INSTALL="pymbolic lark-parser"
- py_version=3
- curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/prepare-and-run-pylint.sh
- . ./prepare-and-run-pylint.sh "$CI_PROJECT_NAME" test/test_*.py
Expand All @@ -61,7 +61,7 @@ Pylint:

Documentation:
script:
- EXTRA_INSTALL="numpy"
- EXTRA_INSTALL="numpy lark-parser"
- curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/build-docs.sh
- ". ./build-docs.sh"
tags:
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ include LICENSE
include doc/*rst
include doc/Makefile
include doc/conf.py

include pytools/*.lark
127 changes: 127 additions & 0 deletions pytools/tag.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from dataclasses import dataclass
from typing import Tuple, Any, FrozenSet, Union, Iterable, TypeVar
import importlib

from pytools import memoize

__copyright__ = """
Expand Down Expand Up @@ -40,6 +42,8 @@
.. autoclass:: Taggable
.. autoclass:: Tag
.. autoclass:: UniqueTag
.. autoclass:: ToPythonObjectMapper
.. autofunction:: parse_tag

Supporting Functionality
------------------------
Expand Down Expand Up @@ -268,4 +272,127 @@ def without_tags(self: T_co,

# }}}


# {{{ parse

class CallParams:
"""
Intermediate data structure for :class:`ToPythonObjectMapper`.
"""
def __init__(self, args, kwargs):
self.args = args
self.kwargs = kwargs


class ToPythonObjectMapper:
"""
Map a parsed tree to pythonic objects.
"""
def __init__(self, shortcuts, caller_globals):
super().__init__()
self.shortcuts = shortcuts
self.caller_globals = caller_globals

def map_tag_from_python_class(self, cls, params):
try:
return cls(*params.args, **params.kwargs)
except TypeError as e:
raise TypeError(f"Error while instantiating '{cls.__name__}': {e}")

def map_empty_args_params(self):
return CallParams((), {})

def map_args_only_params(self, args):
return CallParams(args, {})

def map_kwargs_only_params(self, kwargs):
return CallParams((), kwargs)

def map_args_kwargs_params(self, args, kwargs):
return CallParams(args, kwargs)

def map_name(self, tok):
return tok.value

def map_top_level_module(self, modulename):
try:
return self.caller_globals[modulename]
except KeyError:
return importlib.import_module(modulename)

def map_nested_module(self, module, child):
return getattr(module, child)

def map_tag_class(self, module, classname):
return getattr(module, classname)

def map_string(self, text):
return str(text)

def map_int(self, text):
return int(text)

def map_singleton_args(self, arg):
return (arg,)

def map_args(self, args, arg):
return args + (arg,)

def map_kwarg(self, name, arg):
return {name: arg}

def map_kwargs(self, kwargs, kwarg):
assert len(kwarg) == 1
(key, val), = kwarg.items()
if key in kwargs:
raise ValueError(f"keyword argument '{key}' repeated")

updated_kwargs = kwargs.copy()
updated_kwargs[key] = val
return updated_kwargs

def map_tag_from_shortcut(self, name):
name = name[1:] # remove the starting "."
return self.shortcuts[name]


@memoize
def construct_parser():
from lark import Lark
return Lark.open("tags.lark", rel_to=__file__, parser="lalr", start="tag",
cache=True)


def parse_tag(tag_text, shortcuts={}):
"""
Parses a :class:`Tag` from a provided dotted name.
"""
import inspect
from lark import Transformer

class ToPythonObjectMapperMixin(ToPythonObjectMapper, Transformer):
def _call_userfunc(self, tree, new_children=None):
"""
Flattens the arguments before feeding to the mapper methods.
"""
# Assumes tree is already transformed
children = new_children if new_children is not None else tree.children
try:
f = getattr(self, tree.data)
except AttributeError:
return self.__default__(tree.data, children, tree.meta)
else:
# flatten the args
return f(*children)

parser = construct_parser()

caller_globals = inspect.currentframe().f_back.f_globals
tag = ToPythonObjectMapperMixin(shortcuts, caller_globals).transform(
parser.parse(tag_text))

return tag

# }}}

# vim: foldmethod=marker
35 changes: 35 additions & 0 deletions pytools/tags.lark
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
tag: tag_class "(" params ")" -> map_tag_from_python_class
| SHORTCUT -> map_tag_from_shortcut

params: -> map_empty_args_params
| args -> map_args_only_params
| kwargs -> map_kwargs_only_params
| args "," kwargs -> map_args_kwargs_params

?kwargs: kwarg
| kwargs "," kwarg -> map_kwargs

args: arg -> map_singleton_args
| args "," arg -> map_args

kwarg: name "=" arg -> map_kwarg

?arg: tag
| INT -> map_int
| ESCAPED_STRING -> map_string

tag_class: module "." name -> map_tag_class

module: name -> map_top_level_module
| module "." name -> map_nested_module

name: CNAME -> map_name
SHORTCUT: "." ("_"|LETTER) ("_"|LETTER|DIGIT|".")*

%import common.INT
%import common.ESCAPED_STRING
%import common.DIGIT
%import common.LETTER
%import common.CNAME
%import common.WS
%ignore WS
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@
"decorator>=3.2.0",
"appdirs>=1.4.0",
"numpy>=1.6.0",
"dataclasses>=0.7;python_version<='3.6'"
"dataclasses>=0.7;python_version<='3.6'",
],

extras_require={
"tag_parsing": ["lark-parser"],
},

package_data={"pytools": ["py.typed"]},

author="Andreas Kloeckner",
Expand Down
43 changes: 43 additions & 0 deletions test/test_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sys
import pytest
import testlib_tags as testlib # noqa


def test_parse_tags():
from pytools.tag import parse_tag

def assert_same_as_python(tag_text):
assert parse_tag(tag_text) == eval(tag_text)

assert_same_as_python("testlib.FruitNameTag(testlib.MangoTag())")
assert_same_as_python("testlib.LocalAxisTag(0)")
assert_same_as_python('testlib.ColorTag("blue")')
assert_same_as_python('testlib.ColorTag(color="blue")')
assert_same_as_python("testlib.LocalAxisTag(axis=0)")

assert (parse_tag("testlib.FruitNameTag(.mango)",
shortcuts={"mango": testlib.MangoTag(),
"apple": testlib.AppleTag()})
== testlib.FruitNameTag(testlib.MangoTag()))

assert (parse_tag(".l.0",
shortcuts={"l.0": testlib.LocalAxisTag(0)})
== testlib.LocalAxisTag(axis=0))


def test_parse_tag_raises():
from pytools.tag import parse_tag

with pytest.raises(ValueError):
parse_tag('testlib.ColorTag(color="green",color="blue")')

with pytest.raises(TypeError):
parse_tag('testlib.ColorTag(typoed_kwarg_name="typo",color="blue")')


if __name__ == "__main__":
if len(sys.argv) > 1:
exec(sys.argv[1])
else:
from pytest import main
main([__file__])
26 changes: 26 additions & 0 deletions test/testlib_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pytools.tag import Tag, UniqueTag


class FruitNameTag(UniqueTag):
def __init__(self, tag):
self.tag = tag


class MangoTag(Tag):
def __str__(self):
return "mango"


class AppleTag(Tag):
def __str__(self):
return "apple"


class ColorTag(Tag):
def __init__(self, color):
self.color = color


class LocalAxisTag(Tag):
def __init__(self, axis):
self.axis = axis