Skip to content
Closed
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
1 change: 1 addition & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Simon Ruggier (@sruggier)
Tim Gates (@timgates42) <tim.gates@iress.com>
Batuhan Taskaya (@isidentical) <isidentical@gmail.com>
Jocelyn Boullier (@Kazy) <jocelyn@boullier.bzh>
Sahil Rajput (@SahilRajputdotexe)


Note: (@user) means a github user name.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Changelog
Unreleased
++++++++++

- Add support for the ``match`` statement (PEP 634) in the grammars for
Python 3.10 and later

0.8.7 (2026-05-02)
++++++++++++++++++

Expand Down
6 changes: 6 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ def works_ge_py39(each_version):
return Checker(each_version, version_info >= (3, 9))


@pytest.fixture
def works_ge_py310(each_version):
version_info = parse_version_string(each_version)
return Checker(each_version, version_info >= (3, 10))


@pytest.fixture
def works_ge_py312(each_version):
version_info = parse_version_string(each_version)
Expand Down
24 changes: 21 additions & 3 deletions parso/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
complexity of the ``Parser`` (there's another parser sitting inside
``Statement``, which produces ``Array`` and ``Call``).
"""
from typing import Dict, Type
from typing import Dict, FrozenSet, Type

from parso import tree
from parso.pgen2.generator import ReservedString
Expand Down Expand Up @@ -88,7 +88,7 @@ def __repr__(self):

def _token_to_transition(grammar, type_, value):
# Map from token to label
if type_.value.contains_syntax:
if type_.value.contains_syntax and value.__class__ is not PlainName:
# Check for reserved words (keywords)
try:
return grammar.reserved_syntax_strings[value]
Expand All @@ -98,6 +98,14 @@ def _token_to_transition(grammar, type_, value):
return type_


class PlainName(str):
"""
The value of a soft keyword that is used as a name. It is never looked up
as a keyword.
"""
__slots__ = ()


class BaseParser:
"""Parser engine.

Expand All @@ -116,6 +124,10 @@ class BaseParser:
leaf_map: Dict[str, Type[tree.Leaf]] = {}
default_leaf = tree.Leaf

# Keywords that are only reserved where the grammar asks for them and are
# plain names everywhere else.
soft_keywords: FrozenSet[str] = frozenset()

def __init__(self, pgen_grammar, start_nonterminal='file_input', error_recovery=False):
self._pgen_grammar = pgen_grammar
self._start_nonterminal = start_nonterminal
Expand Down Expand Up @@ -180,7 +192,13 @@ def _add_token(self, token):
plan = stack[-1].dfa.transitions[transition]
break
except KeyError:
if stack[-1].dfa.is_final:
if (
value in self.soft_keywords
and type_ in stack[-1].dfa.transitions
):
value = PlainName(value)
transition = type_
elif stack[-1].dfa.is_final:
self._pop()
else:
self.error_recovery(token)
Expand Down
10 changes: 7 additions & 3 deletions parso/python/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,13 @@ def _ends_with_newline(leaf, suffix=''):

def _flows_finished(pgen_grammar, stack):
"""
if, while, for and try might not be finished, because another part might
still be parsed.
if, while, for, try and match might not be finished, because another part
might still be parsed.
"""
for stack_node in stack:
if stack_node.nonterminal in ('if_stmt', 'while_stmt', 'for_stmt', 'try_stmt'):
if stack_node.nonterminal in (
'if_stmt', 'while_stmt', 'for_stmt', 'try_stmt', 'match_stmt'
):
return False
return True

Expand Down Expand Up @@ -223,6 +225,8 @@ def _suite_or_file_input_is_valid(pgen_grammar, stack):
def _is_flow_node(node):
if node.type == 'async_stmt':
node = node.children[1]
if node.type == 'match_stmt':
return True
try:
value = node.children[0].value
except AttributeError:
Expand Down
221 changes: 220 additions & 1 deletion parso/python/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import sys
import warnings
import re
from ast import literal_eval
from contextlib import contextmanager

from parso.normalizer import Normalizer, NormalizerConfig, Issue, Rule
from parso.python.tokenize import _get_token_collection

_BLOCK_STMTS = ('if_stmt', 'while_stmt', 'for_stmt', 'try_stmt', 'with_stmt')
_STAR_EXPR_PARENTS = ('testlist_star_expr', 'testlist_comp', 'exprlist')
_STAR_EXPR_PARENTS = ('testlist_star_expr', 'testlist_comp', 'exprlist', 'subject_expr')
# This is the maximal block size given by python.
_MAX_BLOCK_SIZE = 20
_MAX_INDENT_COUNT = 100
Expand Down Expand Up @@ -1012,6 +1013,224 @@ def is_issue(self, try_stmt):
self.add_issue(default_except, message=self.message)


def _bound_names(pattern):
"""
Yields the names a pattern binds in source order. An or-pattern binds the
names of its first alternative, all alternatives have to bind the same.
"""
type_ = pattern.type
if type_ == 'name':
if pattern.value != '_':
yield pattern
elif type_ == 'or_pattern':
yield from _bound_names(pattern.children[0])
elif type_ == 'as_pattern':
yield from _bound_names(pattern.children[0])
if pattern.children[-1].value != '_':
yield pattern.children[-1]
elif type_ == 'star_pattern':
if pattern.children[1].value != '_':
yield pattern.children[1]
elif type_ in ('class_arg', 'mapping_item'):
yield from _bound_names(pattern.children[-1])
elif type_ == 'name_pattern':
if _is_class_pattern(pattern):
for argument in _class_pattern_arguments(pattern):
yield from _bound_names(argument)
elif hasattr(pattern, 'children'):
for child in pattern.children:
yield from _bound_names(child)


def _is_class_pattern(name_pattern):
return name_pattern.children[-1].value == ')'


def _class_pattern_arguments(name_pattern):
for index, child in enumerate(name_pattern.children):
if child.type == 'operator' and child.value == '(':
return name_pattern.children[index + 1:-1]


def _irrefutable_name(pattern):
"""
Returns the capture name or the wildcard that makes a pattern match every
subject, or ``None`` if the pattern can fail.
"""
type_ = pattern.type
if type_ == 'name':
return pattern
if type_ == 'as_pattern':
return _irrefutable_name(pattern.children[0])
if type_ == 'or_pattern':
for alternative in pattern.children[::2]:
name = _irrefutable_name(alternative)
if name is not None:
return name
if type_ == 'paren_pattern' and len(pattern.children) == 3:
return _irrefutable_name(pattern.children[1])
return None


def _unreachable_message(name):
if name.value == '_':
return "wildcard makes remaining patterns unreachable"
return "name capture %r makes remaining patterns unreachable" % name.value


@ErrorFinder.register_rule(type='match_stmt')
class _MatchStmtRule(SyntaxRule):
def is_issue(self, match_stmt):
cases = [c for c in match_stmt.children if c.type == 'case_block']
for case in cases[:-1]:
if any(c.type == 'guard' for c in case.children):
continue
pattern = case.children[1]
if pattern.type == 'or_pattern':
# The other alternatives are checked in the or-pattern.
pattern = pattern.children[-1]
name = _irrefutable_name(pattern)
if name is not None:
self.add_issue(name, message=_unreachable_message(name))


@ErrorFinder.register_rule(type='case_block')
class _CaseBlockRule(SyntaxRule):
message = "multiple assignments to name %r in pattern"
message_star = "invalid syntax"

def is_issue(self, case_block):
if case_block.children[1].type == 'star_pattern':
self.add_issue(case_block.children[1], message=self.message_star)
return
seen = set()
for name in _bound_names(case_block.children[1]):
if name.value in seen:
self.add_issue(name, message=self.message % name.value)
break
seen.add(name.value)


@ErrorFinder.register_rule(type='or_pattern')
class _OrPatternRule(SyntaxRule):
message = "alternative patterns bind different names"

def is_issue(self, or_pattern):
alternatives = or_pattern.children[::2]
for alternative in alternatives[:-1]:
name = _irrefutable_name(alternative)
if name is not None:
self.add_issue(name, message=_unreachable_message(name))
return

first = {name.value for name in _bound_names(alternatives[0])}
for alternative in alternatives[1:]:
if {name.value for name in _bound_names(alternative)} != first:
self.add_issue(alternative, message=self.message)
return


@ErrorFinder.register_rule(type='as_pattern')
class _AsPatternRule(SyntaxRule):
message = "cannot use '_' as a target"

def is_issue(self, as_pattern):
return as_pattern.children[-1].value == '_'

def get_node(self, as_pattern):
return as_pattern.children[-1]


@ErrorFinder.register_rule(types=('patterns', 'list_pattern', 'paren_pattern'))
class _StarPatternRule(SyntaxRule):
message = "multiple starred names in sequence pattern"
message_group = "invalid syntax"

def is_issue(self, sequence):
stars = [c for c in sequence.children if c.type == 'star_pattern']
if len(stars) > 1:
self.add_issue(stars[1], message=self.message)
elif sequence.type == 'paren_pattern' and stars and len(sequence.children) == 3:
self.add_issue(stars[0], message=self.message_group)


@ErrorFinder.register_rule(type='literal_pattern')
class _ComplexLiteralRule(SyntaxRule):
def is_issue(self, literal):
numbers = [c for c in literal.children if c.type == 'number']
if len(numbers) == 2:
real, imaginary = numbers
if real.value[-1] in 'jJ':
self.add_issue(real, message="real number required in complex literal")
elif imaginary.value[-1] not in 'jJ':
self.add_issue(
imaginary, message="imaginary number required in complex literal"
)


@ErrorFinder.register_rule(type='mapping_pattern')
class _MappingPatternRule(SyntaxRule):
message = "invalid syntax"
message_duplicate = "mapping pattern checks duplicate key (%r)"

@staticmethod
def _is_valid_key(key):
if key.type in ('number', 'string', 'literal_pattern'):
return True
if key.type == 'keyword':
return key.value in ('None', 'True', 'False')
return key.type == 'name_pattern' and not _is_class_pattern(key)

def is_issue(self, mapping_pattern):
keys = set()
for child in mapping_pattern.children:
if child.type == 'name' and child.value == '_':
self.add_issue(child, message=self.message)
if child.type != 'mapping_item':
continue

key = child.children[0]
if not self._is_valid_key(key):
self.add_issue(key, message=self.message)
continue
if key.type == 'name_pattern':
continue
try:
value = literal_eval(key.get_code(include_prefix=False))
except (ValueError, SyntaxError):
continue
if value in keys:
self.add_issue(key, message=self.message_duplicate % (value,))
keys.add(value)


@ErrorFinder.register_rule(type='name_pattern')
class _ClassPatternRule(SyntaxRule):
message = "invalid syntax"
message_positional = "positional patterns follow keyword patterns"
message_repeated = "attribute name repeated in class pattern: %s"

def is_issue(self, name_pattern):
if not _is_class_pattern(name_pattern):
return

keywords = set()
for argument in _class_pattern_arguments(name_pattern):
if argument.type == 'operator':
continue
if argument.type != 'class_arg':
if keywords:
self.add_issue(argument, message=self.message_positional)
continue

keyword = argument.children[0]
if keyword.type != 'name':
self.add_issue(keyword, message=self.message)
elif keyword.value in keywords:
self.add_issue(keyword, message=self.message_repeated % keyword.value)
keywords.add(keyword.value)


@ErrorFinder.register_rule(type='fstring')
class _FStringRule(SyntaxRule):
_fstring_grammar = None
Expand Down
21 changes: 20 additions & 1 deletion parso/python/grammar310.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ global_stmt: 'global' NAME (',' NAME)*
nonlocal_stmt: 'nonlocal' NAME (',' NAME)*
assert_stmt: 'assert' test [',' test]

compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt | match_stmt
async_stmt: 'async' (funcdef | with_stmt | for_stmt)
if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite)* ['else' ':' suite]
while_stmt: 'while' namedexpr_test ':' suite ['else' ':' suite]
Expand All @@ -95,6 +95,25 @@ with_item: test ['as' expr]
except_clause: 'except' [test ['as' NAME]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT

match_stmt: 'match' subject_expr ':' NEWLINE INDENT case_block+ DEDENT
subject_expr: (namedexpr_test | star_expr) (',' (namedexpr_test | star_expr))* [',']
case_block: 'case' patterns [guard] ':' suite
guard: 'if' namedexpr_test
patterns: pattern_item (',' pattern_item)* [',']
pattern_item: star_pattern | as_pattern
star_pattern: '*' NAME
as_pattern: or_pattern ['as' NAME]
or_pattern: closed_pattern ('|' closed_pattern)*
closed_pattern: (literal_pattern | 'None' | 'True' | 'False' | name_pattern
| paren_pattern | list_pattern | mapping_pattern)
literal_pattern: STRING+ | ['-'] NUMBER [('+' | '-') NUMBER]
name_pattern: NAME ('.' NAME)* ['(' [class_arg (',' class_arg)* [',']] ')']
class_arg: as_pattern ['=' as_pattern]
paren_pattern: '(' [pattern_item (',' pattern_item)* [',']] ')'
list_pattern: '[' [pattern_item (',' pattern_item)* [',']] ']'
mapping_pattern: '{' [(mapping_item ',')* ('**' NAME | mapping_item) [',']] '}'
mapping_item: closed_pattern ':' as_pattern

namedexpr_test: test [':=' test]
test: or_test ['if' or_test 'else' test] | lambdef
lambdef: 'lambda' [varargslist] ':' test
Expand Down
Loading
Loading