Skip to content

Commit

Permalink
Run mypy on src
Browse files Browse the repository at this point in the history
  • Loading branch information
drhagen committed Oct 31, 2024
1 parent 6bb1ce8 commit 8adf453
Show file tree
Hide file tree
Showing 26 changed files with 620 additions and 257 deletions.
4 changes: 2 additions & 2 deletions docs/utility_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ assert BooleanParsers.boolean.parse('false') == Success(False)

## `splat(function)`: convert a function of many arguments to take only one list argument

The function `splat(function: Callable[Tuple[*B], A]) -> Callable[Tuple[Tuple[*B]], A]` has a complicated type signature, but does a simple thing. It takes a single function that takes multiple arguments and converts it to a function that takes only one argument, which is a list of all original arguments. It is particularly useful for passing a list of results from a sequential parser `&` to a function that takes each element as an separate argument. By applying `splat` to the function, it now takes the single list that is returned by the sequential parser.
The function `splat(function: Callable[tuple[*B], A]) -> Callable[tuple[tuple[*B]], A]` has a complicated type signature, but does a simple thing. It takes a single function that takes multiple arguments and converts it to a function that takes only one argument, which is a list of all original arguments. It is particularly useful for passing a list of results from a sequential parser `&` to a function that takes each element as an separate argument. By applying `splat` to the function, it now takes the single list that is returned by the sequential parser.

```python
from collections import namedtuple
Expand All @@ -44,7 +44,7 @@ assert UrlParsers.url.parse('https://drhagen.com:443/blog/') == \

## `unsplat(function)`: convert a function of one list argument to take many arguments

The function `unsplat(function: Callable[Tuple[Tuple[*B]], A]) -> Callable[Tuple[*B], A]` does the opposite of `splat`. It takes a single function that takes a single argument that is a list and converts it to a function that takes multiple arguments, each of which was an element of the original list. It is not very useful for writing parsers because the conversion parser always calls its converter function with a single argument, but is included here to complement `splat`.
The function `unsplat(function: Callable[tuple[tuple[*B]], A]) -> Callable[tuple[*B], A]` does the opposite of `splat`. It takes a single function that takes a single argument that is a list and converts it to a function that takes multiple arguments, each of which was an element of the original list. It is not very useful for writing parsers because the conversion parser always calls its converter function with a single argument, but is included here to complement `splat`.

```python
from parsita.util import splat, unsplat
Expand Down
9 changes: 7 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from nox import options, parametrize
from nox_poetry import Session, session

options.sessions = ["test", "coverage", "lint"]
options.sessions = ["test", "coverage", "lint", "type_check"]


@session(python=["3.9", "3.10", "3.11", "3.12", "3.13"])
Expand All @@ -27,6 +27,11 @@ def lint(s: Session, command: list[str]):


@session(venv_backend="none")
def format(s: Session) -> None:
def type_check(s: Session):
s.run("mypy", "src")


@session(venv_backend="none")
def format(s: Session):
s.run("ruff", "check", ".", "--select", "I", "--fix")
s.run("ruff", "format", ".")
60 changes: 59 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pytest-timeout = "*"
# Lint
ruff = "^0.6"

# Type checking
mypy = "^1"

# Docs
mkdocs-material = "^9"

Expand All @@ -49,6 +52,7 @@ exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING",
"@overload",
]

[tool.coverage.paths]
Expand Down Expand Up @@ -85,6 +89,11 @@ extend-ignore = ["F821", "N805"]
"__init__.py" = ["F401"]


[tool.mypy]
strict = true
implicit_reexport = true


[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
89 changes: 59 additions & 30 deletions src/parsita/metaclasses.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
from __future__ import annotations

__all__ = ["ForwardDeclaration", "fwd", "ParserContext"]

import builtins
import inspect
import re
from dataclasses import dataclass
from re import Pattern
from typing import Any, Union
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, Union, no_type_check

from . import options
from .parsers import LiteralParser, Parser, RegexParser
from .state import Input
from .state import Continue, Input, Output, Reader, State

missing: Any = object()


missing = object()
@dataclass(frozen=True)
class Options:
whitespace: Optional[Parser[Any, Any]] = None


class ParsersDict(dict):
def __init__(self, old_options: dict):
class ParsersDict(dict[str, Any]):
def __init__(self, old_options: Options):
super().__init__()
self.old_options = old_options # Holds state of options at start of definition
self.forward_declarations = {} # Stores forward declarations as they are discovered

def __missing__(self, key):
# Holds state of options at start of definition
self.old_options = old_options

# Stores forward declarations as they are discovered
self.forward_declarations: dict[str, ForwardDeclaration[Any, Any]] = {}

@no_type_check # mypy cannot handle all the frame inspection
def __missing__(self, key: str) -> ForwardDeclaration[Any, Any]:
frame = inspect.currentframe() # Should be the frame of __missing__
while frame.f_code.co_name != "__missing__": # pragma: no cover
# But sometimes debuggers add frames on top of the stack;
Expand All @@ -43,7 +56,7 @@ def __missing__(self, key):
self.forward_declarations[key] = new_forward_declaration
return new_forward_declaration

def __setitem__(self, key, value):
def __setitem__(self, key: str, value: Any) -> None:
if isinstance(value, Parser):
# Protects against accidental concatenation of sequential parsers
value.protected = True
Expand All @@ -54,21 +67,29 @@ def __setitem__(self, key, value):
super().__setitem__(key, value)


class ForwardDeclaration(Parser):
def __init__(self):
self._definition = None
class ForwardDeclaration(Generic[Input, Output], Parser[Input, Output]):
def __init__(self) -> None:
self._definition: Optional[Parser[Input, Output]] = None

def __getattribute__(self, member):
def __getattribute__(self, member: str) -> Any:
if member != "_definition" and self._definition is not None:
return getattr(self._definition, member)
else:
return object.__getattribute__(self, member)

def define(self, parser: Parser) -> None:
if TYPE_CHECKING:
# Type checkers don't know that `_consume` is implemented in `__getattribute__`

def _consume(
self, state: State, reader: Reader[Input]
) -> Optional[Continue[Input, Output]]:
pass

def define(self, parser: Parser[Input, Output]) -> None:
self._definition = parser


def fwd() -> ForwardDeclaration:
def fwd() -> ForwardDeclaration[Input, Output]:
"""Manually create a forward declaration.
Normally, forward declarations are created automatically by the contexts.
Expand All @@ -79,16 +100,20 @@ def fwd() -> ForwardDeclaration:


class ParserContextMeta(type):
default_whitespace: Union[Parser[Input, Any], Pattern, str, None] = None
default_whitespace: Union[Parser[Any, Any], Pattern[str], str, None] = None

@classmethod
def __prepare__(
mcs, # noqa: N804
name,
bases,
name: str,
bases: tuple[type, ...],
/,
*,
whitespace: Union[Parser[Input, Any], Pattern, str, None] = missing,
):
whitespace: Union[Parser[Any, Any], Pattern[str], str, None] = missing,
**kwargs: Any,
) -> ParsersDict:
super().__prepare__(name, bases, **kwargs)

if whitespace is missing:
whitespace = mcs.default_whitespace

Expand All @@ -98,15 +123,13 @@ def __prepare__(
if isinstance(whitespace, Pattern):
whitespace = RegexParser(whitespace)

old_options = {
"whitespace": options.whitespace,
}
old_options = Options(whitespace=options.whitespace)

# Store whitespace in global location
options.whitespace = whitespace
return ParsersDict(old_options)

def __init__(cls, name, bases, dct, **_):
def __init__(cls, name: str, bases: tuple[type, ...], dct: ParsersDict, /, **_: Any) -> None:
old_options = dct.old_options

super().__init__(name, bases, dct)
Expand All @@ -119,15 +142,21 @@ def __init__(cls, name, bases, dct, **_):
forward_declaration._definition = obj

# Reset global variables
for key, value in old_options.items():
setattr(options, key, value)

def __new__(mcs, name, bases, dct, **_): # noqa: N804
options.whitespace = old_options.whitespace

def __new__(
mcs: type[ParserContextMeta], # noqa: N804
name: str,
bases: tuple[type, ...],
dct: ParsersDict,
/,
whitespace: Union[Parser[Any, Any], Pattern[str], str, None] = missing,
) -> ParserContextMeta:
return super().__new__(mcs, name, bases, dct)

def __call__(cls, *args, **kwargs):
def __call__(cls, *args: object, **kwargs: object) -> NoReturn:
raise TypeError(
"Parsers cannot be instantiated. They use class bodies purely as contexts for "
"ParserContexts cannot be instantiated. They use class bodies purely as contexts for "
"managing defaults and allowing forward declarations. Access the individual parsers "
"as static attributes."
)
Expand Down
5 changes: 2 additions & 3 deletions src/parsita/options.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
__all__ = ["whitespace"]

from typing import Any
from typing import Any, Optional

from .parsers import Parser
from .state import Input

# Global mutable state
whitespace: Parser[Input, Any] = None
whitespace: Optional[Parser[Any, Any]] = None
Loading

0 comments on commit 8adf453

Please sign in to comment.