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 type annotations to the python codebase #283

Merged
merged 30 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a7bfe67
Add mypy config
youtux Sep 15, 2024
aae218a
Add some type annotations
youtux Sep 15, 2024
d52237c
Add types to python parser
youtux Sep 15, 2024
0ac8919
Fix types
youtux Sep 15, 2024
f95fe64
Add more type declarations
youtux Sep 15, 2024
fa2d380
Add missing type annotations in `stream/*.py`
youtux Sep 15, 2024
c711abb
WIP start typing ast_builder and ast_node
youtux Sep 15, 2024
65b54c1
Finish typing ast_builder and ast_node
youtux Sep 15, 2024
8f13b7b
Add type annotations for `dialect.py`
youtux Sep 15, 2024
4960655
Add type annotations for `errors.py`
youtux Sep 15, 2024
2d30430
Add type annotations for `gherkin_line.py`
youtux Sep 15, 2024
e2dabec
Remove useless imports
youtux Sep 15, 2024
bb55c95
Remove entry added by mistake
youtux Sep 15, 2024
ab00f5a
Fix annotations for toke_formatter_builder
youtux Sep 15, 2024
314196d
Fix annotations for token_matcher
youtux Sep 15, 2024
d364398
Add type annotations for token_scanner
youtux Sep 16, 2024
36b1be8
Add missing types to token_matcher_markdown
youtux Sep 16, 2024
06ecbbe
Add `py.typed` file marker
youtux Sep 16, 2024
093e34e
Sync parser.py
youtux Sep 16, 2024
2232b6b
Fully type-annotate the `parser.py`
youtux Sep 16, 2024
6835d35
Location.column is not always present
youtux Sep 16, 2024
f506e06
Fix type error
youtux Sep 16, 2024
f720834
Fix python3.8 compatibility
youtux Sep 21, 2024
d1b1d54
Add changelog entry
youtux Sep 21, 2024
bcc29fe
Remove debugging statement committed by mistake
youtux Sep 21, 2024
583f4e1
Fix another python3 compatibility issue
youtux Sep 21, 2024
abed0db
Undo change slipped in by mistake
youtux Sep 21, 2024
32ac8b8
Merge remote-tracking branch 'origin/main' into python-add-type-annot…
youtux Sep 21, 2024
f95ab9a
`Container` -> `Envelope`
youtux Sep 22, 2024
3cda2fb
Remove unnecessary cast
youtux Sep 22, 2024
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

dotnet/.vs/*
dotnet/output
*.iml
*.iml
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
## [Unreleased]
### Added
- [PHP, Java, Ruby, JavaScript] update dependency messages up to v26
- [Python] Added type annotations ([#283](https://github.com/cucumber/gherkin/pull/283))

### Changed
- [.NET] Drop unsupported frameworks. Now supported target frameworks are .NET 8, .NET Standard 2.0 ([#265](https://github.com/cucumber/gherkin/pull/265))
Expand Down
67 changes: 49 additions & 18 deletions python/gherkin-python.razor
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,23 @@
@helper MatchToken(TokenType tokenType)
{<text>match_@(tokenType)(context, token)</text>}
# This file is generated. Do not edit! Edit gherkin-python.razor instead.
import sys
from __future__ import annotations

from collections import deque
from collections.abc import Callable
from typing import cast, TypeVar

from .ast_builder import AstBuilder
from .token import Token
from .token_matcher import TokenMatcher
from .token_scanner import TokenScanner
from .parser_types import GherkinDocument
from .errors import UnexpectedEOFException, UnexpectedTokenException, ParserException, CompositeParserException

_T = TypeVar('_T')
_U = TypeVar('_U')
_V = TypeVar('_V')

RULE_TYPE = [
'None',
@foreach(var rule in Model.RuleSet.Where(r => !r.TempRule))
Expand All @@ -42,19 +52,29 @@ RULE_TYPE = [


class ParserContext:
def __init__(self, token_scanner, token_matcher, token_queue, errors):
def __init__(
self,
token_scanner: TokenScanner,
token_matcher: TokenMatcher,
token_queue: deque[Token],
errors: list[ParserException],
) -> None:
self.token_scanner = token_scanner
self.token_matcher = token_matcher
self.token_queue = token_queue
self.errors = errors


class @(Model.ParserClassName):
def __init__(self, ast_builder=None):
def __init__(self, ast_builder: AstBuilder | None = None) -> None:
self.ast_builder = ast_builder if ast_builder is not None else AstBuilder()
self.stop_at_first_error = False

def parse(self, token_scanner_or_str, token_matcher=None):
def parse(
self,
token_scanner_or_str: TokenScanner | str,
token_matcher: TokenMatcher | None = None,
) -> GherkinDocument:
token_scanner = TokenScanner(token_scanner_or_str) if isinstance(token_scanner_or_str, str) else token_scanner_or_str
self.ast_builder.reset()
if token_matcher is None:
Expand All @@ -80,34 +100,34 @@ class @(Model.ParserClassName):
if context.errors:
raise CompositeParserException(context.errors)

return self.get_result()
return cast(GherkinDocument, self.get_result())

def build(self, context, token):
def build(self, context: ParserContext, token: Token) -> None:
self.handle_ast_error(context, token, self.ast_builder.build)

def add_error(self, context, error):
def add_error(self, context: ParserContext, error: ParserException) -> None:
if str(error) not in (str(e) for e in context.errors):
context.errors.append(error)
if len(context.errors) > 10:
raise CompositeParserException(context.errors)

def start_rule(self, context, rule_type):
def start_rule(self, context: ParserContext, rule_type: str) -> None:
self.handle_ast_error(context, rule_type, self.ast_builder.start_rule)

def end_rule(self, context, rule_type):
def end_rule(self, context: ParserContext, rule_type: str) -> None:
self.handle_ast_error(context, rule_type, self.ast_builder.end_rule)

def get_result(self):
def get_result(self) -> object:
return self.ast_builder.get_result()

def read_token(self, context):
def read_token(self, context: ParserContext) -> Token:
if context.token_queue:
return context.token_queue.popleft()
else:
return context.token_scanner.read()
@foreach(var rule in Model.RuleSet.TokenRules)
{<text>
def match_@(rule.Name.Replace("#", ""))(self, context, token):
def match_@(rule.Name.Replace("#", ""))(self, context: ParserContext, token: Token) -> bool:
@if (rule.Name != "#EOF")
{
@:if token.eof():
Expand All @@ -116,8 +136,8 @@ class @(Model.ParserClassName):
return self.handle_external_error(context, False, token, context.token_matcher.match_@(rule.Name.Replace("#", "")))</text>
}

def match_token(self, state, token, context):
state_map = {
def match_token(self, state: int, token: Token, context: ParserContext) -> int:
state_map: dict[int, Callable[[Token, ParserContext], int]]= {
@foreach(var state in Model.States.Values.Where(s => !s.IsEndState))
{
@: @state.Id: self.match_token_at_@(state.Id),
Expand All @@ -130,7 +150,7 @@ class @(Model.ParserClassName):
@foreach(var state in Model.States.Values.Where(s => !s.IsEndState))
{<text>
# @Raw(state.Comment)
def match_token_at_@(state.Id)(self, token, context):
def match_token_at_@(state.Id)(self, token: Token, context: ParserContext) -> int:
@foreach(var transition in state.Transitions)
{
@:if self.@MatchToken(transition.TokenType):
Expand All @@ -150,7 +170,7 @@ class @(Model.ParserClassName):
@foreach(var lookAheadHint in Model.RuleSet.LookAheadHints)
{
<text>
def lookahead_@(lookAheadHint.Id)(self, context, currentToken):
def lookahead_@(lookAheadHint.Id)(self, context: ParserContext, currentToken: Token) -> bool:
currentToken.detach
token = None
queue = []
Expand All @@ -174,10 +194,21 @@ class @(Model.ParserClassName):

# private

def handle_ast_error(self, context, argument, action):
def handle_ast_error(
self,
context: ParserContext,
argument: _T,
action: Callable[[_T], object],
) -> None:
self.handle_external_error(context, True, argument, action)

def handle_external_error(self, context, default_value, argument, action):
def handle_external_error(
self,
context: ParserContext,
default_value: _U,
argument: _T,
action: Callable[[_T], _V],
) -> _V | _U:
if self.stop_at_first_error:
return action(argument)

Expand Down
8 changes: 7 additions & 1 deletion python/gherkin/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
(options, args) = parser.parse_args()

source_events = SourceEvents(args)
gherkin_events = GherkinEvents(options)
gherkin_events = GherkinEvents(
GherkinEvents.Options(
print_source=options.print_source,
print_ast=options.print_ast,
print_pickles=options.print_pickles,
)
)

for source_event in source_events.enum():
for event in gherkin_events.enum(source_event):
Expand Down
Loading