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

Fix for PR #1149 #1150

Merged
merged 4 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
12 changes: 5 additions & 7 deletions .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@ jobs:
OS: ${{ matrix.os }}
PYTHON: '3.7'
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v3
- name: Download submodules
run: |
git submodule update --init --recursive
git submodule sync -q
git submodule update --init
with:
submodules: recursive
- name: Setup Python
uses: actions/setup-python@master
uses: actions/setup-python@v3
with:
python-version: 3.7
python-version: "3.7"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
14 changes: 3 additions & 11 deletions .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Download submodules
run: git submodule update --init --recursive
- name: Set up Python
uses: actions/setup-python@v1
- uses: actions/checkout@v3
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install mypy
submodules: recursive
- name: Lint with mypy
run: mypy -p lark || true
run: pipx run tox -e type
15 changes: 6 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-rc - 3.10, 3.11-dev, pypy3]
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11-dev", "pypy-3.7"]

steps:
- uses: actions/checkout@v2
- name: Download submodules
run: |
git submodule update --init --recursive
git submodule sync -q
git submodule update --init
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -25,4 +22,4 @@ jobs:
pip install -r test-requirements.txt
- name: Run tests
run: |
python -m tests
python -m tests
13 changes: 8 additions & 5 deletions lark/lark.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
import sys, os, pickle, hashlib
import tempfile
import types
from typing import (
TypeVar, Type, List, Dict, Iterator, Callable, Union, Optional, Sequence,
Tuple, Iterable, IO, Any, TYPE_CHECKING, Collection
Expand All @@ -27,9 +28,10 @@

import re
try:
import regex # type: ignore
import regex
_has_regex = True
except ImportError:
regex = None
_has_regex = False


###{standalone
Expand Down Expand Up @@ -253,11 +255,12 @@ class Lark(Serialize):

def __init__(self, grammar: 'Union[Grammar, str, IO[str]]', **options) -> None:
self.options = LarkOptions(options)
re_module: types.ModuleType

# Set regex or re module
use_regex = self.options.regex
if use_regex:
if regex:
if _has_regex:
re_module = regex
else:
raise ImportError('`regex` module must be installed if calling `Lark(regex=True)`.')
Expand All @@ -267,15 +270,15 @@ def __init__(self, grammar: 'Union[Grammar, str, IO[str]]', **options) -> None:
# Some, but not all file-like objects have a 'name' attribute
if self.options.source_path is None:
try:
self.source_path = grammar.name
self.source_path = grammar.name # type: ignore[union-attr]
except AttributeError:
self.source_path = '<string>'
else:
self.source_path = self.options.source_path

# Drain file-like objects to get their contents
try:
read = grammar.read
read = grammar.read # type: ignore[union-attr]
except AttributeError:
pass
else:
Expand Down
1 change: 1 addition & 0 deletions lark/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

if TYPE_CHECKING:
from .lexer import TerminalDef, Token
import rich
if sys.version_info >= (3, 8):
from typing import Literal
else:
Expand Down
20 changes: 15 additions & 5 deletions lark/tree_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@
from lark import Tree, Transformer
from lark.exceptions import MissingVariableError

Branch = Union[Tree[str], str]
TreeOrCode = Union[Tree[str], str]
MatchResult = Dict[str, Tree]
_TEMPLATE_MARKER = '$'


class TemplateConf:
"""Template Configuration

Allows customization for different uses of Template

parse() must return a Tree instance.
"""

def __init__(self, parse=None):
Copy link
Member

Choose a reason for hiding this comment

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

This could be annotated with Callable[[str], Tree]

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, but there's a lot of "could be" to add there. We can do that in a different PR.

Expand Down Expand Up @@ -49,15 +53,21 @@ def _get_tree(self, template: TreeOrCode) -> Tree[str]:
assert self._parse
template = self._parse(template)

assert isinstance(template, Tree)
if not isinstance(template, Tree):
raise TypeError("template parser must return a Tree instance")

return template

def __call__(self, template: Tree[str]) -> 'Template':
return Template(template, conf=self)

def _match_tree_template(self, template: TreeOrCode, tree: TreeOrCode) -> Optional[Dict[str, TreeOrCode]]:
def _match_tree_template(self, template: TreeOrCode, tree: Branch) -> Optional[MatchResult]:
"""Returns dict of {var: match} if found a match, else None
"""
template_var = self.test_var(template)
if template_var:
if not isinstance(tree, Tree):
raise TypeError(f"Template variables can only match Tree instances. Not {tree}")
Copy link
Member

Choose a reason for hiding this comment

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

This should be either type(tree) or {tree!r}

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

return {template_var: tree}

if isinstance(template, str):
Expand Down Expand Up @@ -100,7 +110,7 @@ def __default__(self, data, children, meta) -> Tree[str]:


class Template:
"""Represents a tree templates, tied to a specific configuration
"""Represents a tree template, tied to a specific configuration

A tree template is a tree that contains nodes that are template variables.
Those variables will match any tree.
Expand All @@ -111,7 +121,7 @@ def __init__(self, tree: Tree[str], conf: TemplateConf = TemplateConf()):
self.conf = conf
self.tree = conf._get_tree(tree)

def match(self, tree: TreeOrCode) -> Optional[Dict[str, TreeOrCode]]:
def match(self, tree: TreeOrCode) -> Optional[MatchResult]:
"""Match a tree template to a tree.

A tree template without variables will only match ``tree`` if it is equal to the template.
Expand All @@ -127,7 +137,7 @@ def match(self, tree: TreeOrCode) -> Optional[Dict[str, TreeOrCode]]:
tree = self.conf._get_tree(tree)
return self.conf._match_tree_template(self.tree, tree)

def search(self, tree: TreeOrCode) -> Iterator[Tuple[Tree[str], Dict[str, TreeOrCode]]]:
def search(self, tree: TreeOrCode) -> Iterator[Tuple[Tree[str], MatchResult]]:
"""Search for all occurances of the tree template inside ``tree``.
"""
tree = self.conf._get_tree(tree)
Expand Down
14 changes: 8 additions & 6 deletions lark/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,10 @@ def deserialize(cls, data, namespace, memo):


try:
import regex # type: ignore
import regex
_has_regex = True
except ImportError:
regex = None
_has_regex = False

if sys.version_info >= (3, 11):
import re._parser as sre_parse
Expand All @@ -123,7 +124,7 @@ def deserialize(cls, data, namespace, memo):
categ_pattern = re.compile(r'\\p{[A-Za-z_]+}')

def get_regexp_width(expr):
if regex:
if _has_regex:
# Since `sre_parse` cannot deal with Unicode categories of the form `\p{Mn}`, we replace these with
# a simple letter, which makes no difference as we are only trying to get the possible lengths of the regex
# match here below.
Expand All @@ -135,7 +136,7 @@ def get_regexp_width(expr):
try:
return [int(x) for x in sre_parse.parse(regexp_final).getwidth()]
except sre_constants.error:
if not regex:
if not _has_regex:
raise ValueError(expr)
else:
# sre_parse does not support the new features in regex. To not completely fail in that case,
Expand Down Expand Up @@ -223,15 +224,16 @@ def combine_alternatives(lists):

try:
import atomicwrites
_has_atomicwrites = True
except ImportError:
atomicwrites = None # type: ignore[assigment]
_has_atomicwrites = False

class FS:
exists = staticmethod(os.path.exists)

@staticmethod
def open(name, mode="r", **kwargs):
if atomicwrites and "w" in mode:
if _has_atomicwrites and "w" in mode:
return atomicwrites.atomic_write(name, mode=mode, overwrite=True, **kwargs)
else:
return open(name, mode, **kwargs)
Expand Down
18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"


[tool.mypy]
files = "lark"
python_version = "3.6"
show_error_codes = true
enable_error_code = ["ignore-without-code"]
exclude = [
"^lark/__pyinstaller",
]

# You can disable imports or control per-module/file settings here
[[tool.mypy.overrides]]
module = [ "js2py" ]
ignore_missing_imports = true
2 changes: 1 addition & 1 deletion tests/test_nearley/nearley
6 changes: 6 additions & 0 deletions tests/test_tree_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ def test_template_match__default_conf_match_same_tree__empty_dictionary(self):

self.assertEqual({}, template.match(SOME_NON_TEMPLATE_TREE))

def test_template_match__only_tree(self):
"This test might become irrelevant in the future"
template_tree = Tree('bar', [Tree("var", children=["$foo"])])
template = Template(template_tree)
self.assertRaises(TypeError, template.match, Tree('bar', ['BAD']))


class TestTreeTemplatesTemplate(unittest.TestCase):
parser = Lark(SOME_TEMPLATING_GRAMMAR)
Expand Down
22 changes: 18 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
[tox]
envlist = py36, py37, py38, py39, pypy, pypy3
skip_missing_interpreters=true
envlist = py36, py37, py38, py39, py310, pypy3, type
skip_missing_interpreters = true

[testenv]
whitelist_externals = git
deps =
-rtest-requirements.txt
passenv =
TERM

# to always force recreation and avoid unexpected side effects
recreate=True
recreate = True

commands=
commands =
git submodule sync -q
git submodule update --init
python -m tests {posargs}

[testenv:type]
description = run type check on code base
skip_install = true
recreate = false
deps =
mypy==0.950
types-atomicwrites
types-regex
rich
commands =
mypy