Skip to content

Commit

Permalink
Fix some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
drhagen committed Nov 19, 2024
1 parent e07b6eb commit cbbca87
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 88 deletions.
44 changes: 24 additions & 20 deletions examples/expressions.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
from typing import Literal, Sequence

from parsita import ParserContext, lit, opt, reg, rep


def make_term(args: tuple[float, Sequence[tuple[Literal["*", "/"], float]]]) -> float:
factor1, factors = args
result = factor1
for op, factor in factors:
if op == "*":
result = result * factor
else:
result = result / factor
return result


def make_expr(args: tuple[float, Sequence[tuple[Literal["+", "-"], float]]]) -> float:
term1, terms = args
result = term1
for op, term2 in terms:
if op == "+":
result = result + term2
else:
result = result - term2
return result


class ExpressionParsers(ParserContext, whitespace=r"[ ]*"):
number = reg(r"[+-]?\d+(\.\d+)?(e[+-]?\d+)?") > float

base = "(" >> expr << ")" | number

factor = base & opt("^" >> base) > (lambda x: x[0] ** x[1][0] if x[1] else x[0])

def make_term(args):
factor1, factors = args
result = factor1
for op, factor in factors:
if op == "*":
result = result * factor
else:
result = result / factor
return result

term = factor & rep(lit("*", "/") & factor) > make_term

def make_expr(args):
term1, terms = args
result = term1
for op, term2 in terms:
if op == "+":
result = result + term2
else:
result = result - term2
return result

expr = term & rep(lit("+", "-") & term) > make_expr


Expand Down
3 changes: 2 additions & 1 deletion examples/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class JsonStringParsers(ParserContext):
line_feed = lit(r"\n") > constant("\n")
carriage_return = lit(r"\r") > constant("\r")
tab = lit(r"\t") > constant("\t")
uni = reg(r"\\u([0-9a-fA-F]{4})") > (lambda x: chr(int(x.group(1), 16)))
uni = reg(r"\\u[0-9a-fA-F]{4}") > (lambda x: chr(int(x[2:], 16)))

escaped = (
quote
Expand Down Expand Up @@ -61,6 +61,7 @@ class JsonParsers(ParserContext, whitespace=r"[ \t\n\r]*"):
"width" : 4.0
}""",
'{"text" : ""}',
r'"\u2260"',
]

for string in strings:
Expand Down
22 changes: 11 additions & 11 deletions examples/positioned.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from abc import abstractmethod
from dataclasses import dataclass
from typing import Generic
from typing import Generic, Optional

from parsita import Parser, ParserContext, Reader, reg
from parsita.state import Continue, Input, Output, State
Expand Down Expand Up @@ -45,7 +45,7 @@ def __init__(self, parser: Parser[Input, PositionAware[Output]]):
super().__init__()
self.parser = parser

def _consume(self, state: State, reader: Reader[Input]):
def _consume(self, state: State, reader: Reader[Input]) -> Optional[Continue[Input, Output]]:
start = reader.position
status = self.parser.consume(state, reader)

Expand All @@ -55,11 +55,11 @@ def _consume(self, state: State, reader: Reader[Input]):
else:
return status

def __repr__(self):
def __repr__(self) -> str:
return self.name_or_nothing() + f"positioned({self.parser.name_or_repr()})"


def positioned(parser: Parser[Input, PositionAware[Output]]):
def positioned(parser: Parser[Input, PositionAware[Output]]) -> PositionedParser[Input, Output]:
"""Set the position on a PositionAware value.
This parser matches ``parser`` and, if successful, calls ``set_position``
Expand All @@ -75,18 +75,18 @@ def positioned(parser: Parser[Input, PositionAware[Output]]):

# Everything below here is an example use case
@dataclass
class UnfinishedVariable(PositionAware):
class Variable:
name: str

def set_position(self, start: int, length: int):
return Variable(self.name, start, length)
start: int
length: int


@dataclass
class Variable:
class UnfinishedVariable(PositionAware[Variable]):
name: str
start: int
length: int

def set_position(self, start: int, length: int) -> Variable:
return Variable(self.name, start, length)


@dataclass
Expand Down
22 changes: 21 additions & 1 deletion src/parsita/parsers/_debug.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__all__ = ["DebugParser", "debug"]

from typing import Callable, Generic, Optional
from typing import Any, Callable, Generic, Optional, Sequence, Union, overload

from ..state import Continue, Input, Output, Reader, State
from ._base import Parser, wrap_literal
Expand Down Expand Up @@ -37,12 +37,32 @@ def __repr__(self) -> str:
return self.name_or_nothing() + f"debug({self.parser.name_or_repr()})"


@overload
def debug(
parser: Sequence[Input],
*,
verbose: bool = False,
callback: Optional[Callable[[Parser[Input, Sequence[Input]], Reader[Input]], None]] = None,
) -> DebugParser[Input, Sequence[Input]]:
pass


@overload
def debug(
parser: Parser[Input, Output],
*,
verbose: bool = False,
callback: Optional[Callable[[Parser[Input, Output], Reader[Input]], None]] = None,
) -> DebugParser[Input, Output]:
pass


def debug(
parser: Union[Parser[Input, Output], Sequence[Input]],
*,
verbose: bool = False,
callback: Optional[Callable[[Parser[Input, Output], Reader[Input]], None]] = None,
) -> DebugParser[Input, Any]:
"""Execute debugging hooks before a parser.
This parser is used purely for debugging purposes. From a parsing
Expand Down
35 changes: 27 additions & 8 deletions src/parsita/parsers/_literal.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
__all__ = ["LiteralParser", "lit"]

from typing import Generic, Optional, Sequence
from typing import Any, Generic, Optional, Sequence, TypeVar, Union, overload

from .. import options
from ..state import Continue, Element, Reader, State, StringReader
from ._base import Parser

# The bound should be Sequence[Element], but mypy doesn't support higher-kinded types.
Literal = TypeVar("Literal", bound=Sequence[Any], covariant=True)

class LiteralParser(Generic[Element], Parser[Element, Sequence[Element]]):
def __init__(
self, pattern: Sequence[Element], whitespace: Optional[Parser[Element, object]] = None
):

class LiteralParser(Generic[Element, Literal], Parser[Element, Literal]):
def __init__(self, pattern: Literal, whitespace: Optional[Parser[Element, object]] = None):
super().__init__()
self.pattern = pattern
self.whitespace = whitespace

def _consume(
self, state: State, reader: Reader[Element]
) -> Optional[Continue[Element, Sequence[Element]]]:
) -> Optional[Continue[Element, Literal]]:
if self.whitespace is not None:
status = self.whitespace.consume(state, reader)
reader = status.remainder # type: ignore # whitespace is infallible
Expand Down Expand Up @@ -49,9 +50,27 @@ def __repr__(self) -> str:
return self.name_or_nothing() + repr(self.pattern)


FunctionLiteral = TypeVar("FunctionLiteral", bound=Sequence[Any])


@overload
def lit(literal: str, *literals: str) -> Parser[str, str]:
pass


@overload
def lit(literal: bytes, *literals: bytes) -> Parser[int, bytes]:
pass


@overload
def lit(literal: FunctionLiteral, *literals: FunctionLiteral) -> Parser[Any, FunctionLiteral]:
pass


def lit(
literal: Sequence[Element], *literals: Sequence[Element]
) -> Parser[Element, Sequence[Element]]:
literal: Union[FunctionLiteral, str, bytes], *literals: Union[FunctionLiteral, str, bytes]
) -> Parser[Element, object]:
"""Match a literal sequence.
This parser returns successfully if the subsequence of the parsing Element
Expand Down
6 changes: 4 additions & 2 deletions src/parsita/parsers/_regex.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
__all__ = ["RegexParser", "reg"]

import re
from typing import Generic, Optional, TypeVar, Union, no_type_check
from typing import Any, Generic, Optional, TypeVar, Union, no_type_check

from .. import options
from ..state import Continue, State, StringReader
Expand All @@ -10,7 +10,9 @@
StringType = TypeVar("StringType", str, bytes)


class RegexParser(Generic[StringType], Parser[StringType, StringType]):
# The Element type is str for str and int for bytes, but there is not way to
# express that in Python.
class RegexParser(Generic[StringType], Parser[Any, StringType]):
def __init__(
self,
pattern: re.Pattern[StringType],
Expand Down
4 changes: 2 additions & 2 deletions src/parsita/parsers/_until.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__all__ = ["UntilParser", "until"]

from typing import Any, Generic, Optional, Sequence
from typing import Any, Generic, Optional, Sequence, Union

from ..state import Continue, Element, Reader, State
from ._base import Parser, wrap_literal
Expand Down Expand Up @@ -31,7 +31,7 @@ def __repr__(self) -> str:
return self.name_or_nothing() + f"until({self.parser.name_or_repr()})"


def until(parser: Parser[Element, object]) -> UntilParser[Element]:
def until(parser: Union[Parser[Element, object], Sequence[Element]]) -> UntilParser[Element]:
"""Match everything until it matches the provided parser.
This parser matches all Element until it encounters a position in the Element
Expand Down
13 changes: 6 additions & 7 deletions src/parsita/state/_result.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__all__ = ["Result", "Success", "Failure"]

from typing import TYPE_CHECKING, TypeVar
from typing import TypeVar

from returns import result

Expand All @@ -9,11 +9,10 @@
Output = TypeVar("Output")

# Reexport Returns Result types
# Failure and Result fail in isinstance
# Failure is replaced by plain Failure, which works at runtime
# Result is left as is because cannot be fixed without breaking eager type annotations
Result = result.Result[Output, ParseError]
Success = result.Success
if TYPE_CHECKING:
# This object fails in isinstance
# Result does too, but that cannot be fixed without breaking eager type annotations
Failure = result.Failure[ParseError]
else:
Failure = result.Failure
Failure: type[result.Failure[ParseError]] = result.Failure[ParseError]
Failure = result.Failure
7 changes: 4 additions & 3 deletions src/parsita/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

__all__ = ["constant", "splat", "unsplat"]

from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Any, Callable, Sequence

if TYPE_CHECKING:
# ParamSpec was introduced in Python 3.10
Expand Down Expand Up @@ -31,7 +31,8 @@ def constanted(*args: P.args, **kwargs: P.kwargs) -> A:
return constanted


def splat(f: Callable[[Unpack[Ts]], A], /) -> Callable[[tuple[Unpack[Ts]]], A]:
# This signature cannot be expressed narrowly because SequenceParser does not return a tuple
def splat(f: Callable[[Unpack[Ts]], A], /) -> Callable[[Sequence[Any]], A]:
"""Convert a function of multiple arguments into a function of a single iterable argument.
Args:
Expand All @@ -50,7 +51,7 @@ def splat(f: Callable[[Unpack[Ts]], A], /) -> Callable[[tuple[Unpack[Ts]]], A]:
$ g([1, 2, 3]) # 6
"""

def splatted(args: tuple[Unpack[Ts]], /) -> A:
def splatted(args: Sequence[Any], /) -> A:
return f(*args)

return splatted
Expand Down
24 changes: 13 additions & 11 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union

import pytest

from parsita import (
Expand Down Expand Up @@ -424,14 +426,14 @@ class TestParsers(ParserContext):


def test_conversion():
def make_twentyone(x: tuple[float, float]) -> float:
return x[0] * 10 + x[1]

class TestParsers(ParserContext):
one = lit("1") > int
two = lit("2") > int
twelve = one & two > (lambda x: x[0] * 10 + x[1])

def make_twentyone(x):
return x[0] * 10 + x[1]

twentyone = two & one > make_twentyone

assert TestParsers.one.parse("1") == Success(1)
Expand All @@ -442,21 +444,21 @@ def make_twentyone(x):


def test_recursion():
def make_expr(x: tuple[float, Union[tuple[()], tuple[float]]]) -> float:
digits1, maybe_expr = x
if maybe_expr:
digits2 = maybe_expr[0]
return digits1 + digits2
else:
return digits1

class TestParsers(ParserContext):
one = lit("1") > float
six = lit("6") > float
eleven = lit("11") > float

numbers = eleven | one | six

def make_expr(x):
digits1, maybe_expr = x
if maybe_expr:
digits2 = maybe_expr[0]
return digits1 + digits2
else:
return digits1

expr = numbers & opt("+" >> expr) > make_expr

assert TestParsers.expr.parse("11") == Success(11)
Expand Down
Loading

0 comments on commit cbbca87

Please sign in to comment.