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 some helpers to deal with streams of bytes #81

Merged
merged 3 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://gitlab.com/pycqa/flake8
- repo: https://github.com/pycqa/flake8
rev: 3.8.4
hooks:
- id: flake8
Expand Down
26 changes: 21 additions & 5 deletions src/parsy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re
from dataclasses import dataclass
from functools import wraps
from typing import Any, Callable, FrozenSet
from typing import Any, AnyStr, Callable, FrozenSet

__version__ = "2.1"

Expand Down Expand Up @@ -166,10 +166,23 @@ def combine_dict(self, combine_fn: Callable) -> Parser:

def concat(self) -> Parser:
"""
Returns a parser that concatenates together (as a string) the previously
produced values.
Returns a parser that concatenates together the previously produced values.

This parser will join the values using the type of the input stream, so
when feeding bytes to the parser, the items to be joined must also be bytes.
"""
return self.map("".join)

@Parser
def parser(stream: bytes | str, index: int) -> Result:

Choose a reason for hiding this comment

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

this could be typing.AnyStr btw. (that would retain the type if Result would become a generic type)

joiner = type(stream)()
result = self(stream, index)
if result.status:
next_parser: Parser = success(joiner.join(result.value))
return next_parser(stream, result.index).aggregate(result)
else:
return result

return parser

def then(self, other: Parser) -> Parser:
"""
Expand Down Expand Up @@ -516,13 +529,16 @@ def fail(expected: str) -> Parser:
return Parser(lambda _, index: Result.failure(index, expected))


def string(expected_string: str, transform: Callable[[str], str] = noop) -> Parser:
def string(expected_string: AnyStr, transform: Callable[[AnyStr], AnyStr] = noop) -> Parser:
"""
Returns a parser that expects the ``expected_string`` and produces
that string value.

Optionally, a transform function can be passed, which will be used on both
the expected string and tested string.

This parser can also be instantiated with a bytes value, in which case it can
should be applied to a stream of bytes.
"""

slen = len(expected_string)
Expand Down
24 changes: 21 additions & 3 deletions tests/test_parsy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@


class TestParser(unittest.TestCase):
def test_string(self):
def test_string_str(self):
parser = string("x")
self.assertEqual(parser.parse("x"), "x")

self.assertRaises(ParseError, parser.parse, "y")

def test_string_transform(self):
def test_string_transform_str(self):
parser = string("x", transform=lambda s: s.lower())
self.assertEqual(parser.parse("x"), "x")
self.assertEqual(parser.parse("X"), "x")
Expand All @@ -53,6 +53,19 @@ def test_string_transform_2(self):

self.assertRaises(ParseError, parser.parse, "dog")

def test_string_bytes(self):
parser = string(b"x")
self.assertEqual(parser.parse(b"x"), b"x")

self.assertRaises(ParseError, parser.parse, b"y")

def test_string_transform_bytes(self):
parser = string(b"x", transform=lambda s: s.lower())
self.assertEqual(parser.parse(b"x"), b"x")
self.assertEqual(parser.parse(b"X"), b"x")

self.assertRaises(ParseError, parser.parse, b"y")

def test_regex_str(self):
parser = regex(r"[0-9]")

Expand Down Expand Up @@ -157,11 +170,16 @@ def test_combine_dict_skip_underscores(self):
).combine_dict(Pair)
self.assertEqual(parser.parse("ABC 123"), Pair(word="ABC", number=123))

def test_concat(self):
def test_concat_str(self):
parser = letter.many().concat()
self.assertEqual(parser.parse(""), "")
self.assertEqual(parser.parse("abc"), "abc")

def test_concat_bytes(self):
parser = any_char.many().concat()
self.assertEqual(parser.parse(b""), b"")
self.assertEqual(parser.parse(b"abc"), b"abc")

def test_generate(self):
x = y = None

Expand Down