Skip to content

Commit 71b4044

Browse files
gh-104683: Modernise Argument Clinic parameter state machine (#106362)
Use enums and pattern matching to make the code more readable. Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent 7709037 commit 71b4044

File tree

1 file changed

+78
-51
lines changed

1 file changed

+78
-51
lines changed

Tools/clinic/clinic.py

Lines changed: 78 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4294,6 +4294,37 @@ def dedent(self, line):
42944294
StateKeeper = Callable[[str | None], None]
42954295
ConverterArgs = dict[str, Any]
42964296

4297+
class ParamState(enum.IntEnum):
4298+
"""Parameter parsing state.
4299+
4300+
[ [ a, b, ] c, ] d, e, f=3, [ g, h, [ i ] ] <- line
4301+
01 2 3 4 5 6 <- state transitions
4302+
"""
4303+
# Before we've seen anything.
4304+
# Legal transitions: to LEFT_SQUARE_BEFORE or REQUIRED
4305+
START = 0
4306+
4307+
# Left square backets before required params.
4308+
LEFT_SQUARE_BEFORE = 1
4309+
4310+
# In a group, before required params.
4311+
GROUP_BEFORE = 2
4312+
4313+
# Required params, positional-or-keyword or positional-only (we
4314+
# don't know yet). Renumber left groups!
4315+
REQUIRED = 3
4316+
4317+
# Positional-or-keyword or positional-only params that now must have
4318+
# default values.
4319+
OPTIONAL = 4
4320+
4321+
# In a group, after required params.
4322+
GROUP_AFTER = 5
4323+
4324+
# Right square brackets after required params.
4325+
RIGHT_SQUARE_AFTER = 6
4326+
4327+
42974328
class DSLParser:
42984329
function: Function | None
42994330
state: StateKeeper
@@ -4331,7 +4362,7 @@ def reset(self) -> None:
43314362
self.keyword_only = False
43324363
self.positional_only = False
43334364
self.group = 0
4334-
self.parameter_state = self.ps_start
4365+
self.parameter_state: ParamState = ParamState.START
43354366
self.seen_positional_with_default = False
43364367
self.indent = IndentStack()
43374368
self.kind = CALLABLE
@@ -4726,22 +4757,8 @@ def state_modulename_name(self, line: str | None) -> None:
47264757
#
47274758
# These rules are enforced with a single state variable:
47284759
# "parameter_state". (Previously the code was a miasma of ifs and
4729-
# separate boolean state variables.) The states are:
4730-
#
4731-
# [ [ a, b, ] c, ] d, e, f=3, [ g, h, [ i ] ] <- line
4732-
# 01 2 3 4 5 6 <- state transitions
4733-
#
4734-
# 0: ps_start. before we've seen anything. legal transitions are to 1 or 3.
4735-
# 1: ps_left_square_before. left square brackets before required parameters.
4736-
# 2: ps_group_before. in a group, before required parameters.
4737-
# 3: ps_required. required parameters, positional-or-keyword or positional-only
4738-
# (we don't know yet). (renumber left groups!)
4739-
# 4: ps_optional. positional-or-keyword or positional-only parameters that
4740-
# now must have default values.
4741-
# 5: ps_group_after. in a group, after required parameters.
4742-
# 6: ps_right_square_after. right square brackets after required parameters.
4743-
ps_start, ps_left_square_before, ps_group_before, ps_required, \
4744-
ps_optional, ps_group_after, ps_right_square_after = range(7)
4760+
# separate boolean state variables.) The states are defined in the
4761+
# ParamState class.
47454762

47464763
def state_parameters_start(self, line: str | None) -> None:
47474764
if not self.valid_line(line):
@@ -4759,8 +4776,8 @@ def to_required(self):
47594776
"""
47604777
Transition to the "required" parameter state.
47614778
"""
4762-
if self.parameter_state != self.ps_required:
4763-
self.parameter_state = self.ps_required
4779+
if self.parameter_state is not ParamState.REQUIRED:
4780+
self.parameter_state = ParamState.REQUIRED
47644781
for p in self.function.parameters.values():
47654782
p.group = -p.group
47664783

@@ -4793,17 +4810,18 @@ def state_parameter(self, line):
47934810
self.parse_special_symbol(line)
47944811
return
47954812

4796-
if self.parameter_state in (self.ps_start, self.ps_required):
4797-
self.to_required()
4798-
elif self.parameter_state == self.ps_left_square_before:
4799-
self.parameter_state = self.ps_group_before
4800-
elif self.parameter_state == self.ps_group_before:
4801-
if not self.group:
4813+
match self.parameter_state:
4814+
case ParamState.START | ParamState.REQUIRED:
48024815
self.to_required()
4803-
elif self.parameter_state in (self.ps_group_after, self.ps_optional):
4804-
pass
4805-
else:
4806-
fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ".a)")
4816+
case ParamState.LEFT_SQUARE_BEFORE:
4817+
self.parameter_state = ParamState.GROUP_BEFORE
4818+
case ParamState.GROUP_BEFORE:
4819+
if not self.group:
4820+
self.to_required()
4821+
case ParamState.GROUP_AFTER | ParamState.OPTIONAL:
4822+
pass
4823+
case st:
4824+
fail(f"Function {self.function.name} has an unsupported group configuration. (Unexpected state {st}.a)")
48074825

48084826
# handle "as" for parameters too
48094827
c_name = None
@@ -4863,8 +4881,9 @@ def state_parameter(self, line):
48634881
name, legacy, kwargs = self.parse_converter(parameter.annotation)
48644882

48654883
if not default:
4866-
if self.parameter_state == self.ps_optional:
4867-
fail("Can't have a parameter without a default (" + repr(parameter_name) + ")\nafter a parameter with a default!")
4884+
if self.parameter_state is ParamState.OPTIONAL:
4885+
fail(f"Can't have a parameter without a default ({parameter_name!r})\n"
4886+
"after a parameter with a default!")
48684887
if is_vararg:
48694888
value = NULL
48704889
kwargs.setdefault('c_default', "NULL")
@@ -4876,8 +4895,8 @@ def state_parameter(self, line):
48764895
if is_vararg:
48774896
fail("Vararg can't take a default value!")
48784897

4879-
if self.parameter_state == self.ps_required:
4880-
self.parameter_state = self.ps_optional
4898+
if self.parameter_state is ParamState.REQUIRED:
4899+
self.parameter_state = ParamState.OPTIONAL
48814900
default = default.strip()
48824901
bad = False
48834902
ast_input = f"x = {default}"
@@ -5001,22 +5020,22 @@ def bad_node(self, node):
50015020

50025021
if isinstance(converter, self_converter):
50035022
if len(self.function.parameters) == 1:
5004-
if (self.parameter_state != self.ps_required):
5023+
if self.parameter_state is not ParamState.REQUIRED:
50055024
fail("A 'self' parameter cannot be marked optional.")
50065025
if value is not unspecified:
50075026
fail("A 'self' parameter cannot have a default value.")
50085027
if self.group:
50095028
fail("A 'self' parameter cannot be in an optional group.")
50105029
kind = inspect.Parameter.POSITIONAL_ONLY
5011-
self.parameter_state = self.ps_start
5030+
self.parameter_state = ParamState.START
50125031
self.function.parameters.clear()
50135032
else:
50145033
fail("A 'self' parameter, if specified, must be the very first thing in the parameter block.")
50155034

50165035
if isinstance(converter, defining_class_converter):
50175036
_lp = len(self.function.parameters)
50185037
if _lp == 1:
5019-
if (self.parameter_state != self.ps_required):
5038+
if self.parameter_state is not ParamState.REQUIRED:
50205039
fail("A 'defining_class' parameter cannot be marked optional.")
50215040
if value is not unspecified:
50225041
fail("A 'defining_class' parameter cannot have a default value.")
@@ -5065,12 +5084,13 @@ def parse_special_symbol(self, symbol):
50655084
fail("Function " + self.function.name + " uses '*' more than once.")
50665085
self.keyword_only = True
50675086
elif symbol == '[':
5068-
if self.parameter_state in (self.ps_start, self.ps_left_square_before):
5069-
self.parameter_state = self.ps_left_square_before
5070-
elif self.parameter_state in (self.ps_required, self.ps_group_after):
5071-
self.parameter_state = self.ps_group_after
5072-
else:
5073-
fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ".b)")
5087+
match self.parameter_state:
5088+
case ParamState.START | ParamState.LEFT_SQUARE_BEFORE:
5089+
self.parameter_state = ParamState.LEFT_SQUARE_BEFORE
5090+
case ParamState.REQUIRED | ParamState.GROUP_AFTER:
5091+
self.parameter_state = ParamState.GROUP_AFTER
5092+
case st:
5093+
fail(f"Function {self.function.name} has an unsupported group configuration. (Unexpected state {st}.b)")
50745094
self.group += 1
50755095
self.function.docstring_only = True
50765096
elif symbol == ']':
@@ -5079,20 +5099,27 @@ def parse_special_symbol(self, symbol):
50795099
if not any(p.group == self.group for p in self.function.parameters.values()):
50805100
fail("Function " + self.function.name + " has an empty group.\nAll groups must contain at least one parameter.")
50815101
self.group -= 1
5082-
if self.parameter_state in (self.ps_left_square_before, self.ps_group_before):
5083-
self.parameter_state = self.ps_group_before
5084-
elif self.parameter_state in (self.ps_group_after, self.ps_right_square_after):
5085-
self.parameter_state = self.ps_right_square_after
5086-
else:
5087-
fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ".c)")
5102+
match self.parameter_state:
5103+
case ParamState.LEFT_SQUARE_BEFORE | ParamState.GROUP_BEFORE:
5104+
self.parameter_state = ParamState.GROUP_BEFORE
5105+
case ParamState.GROUP_AFTER | ParamState.RIGHT_SQUARE_AFTER:
5106+
self.parameter_state = ParamState.RIGHT_SQUARE_AFTER
5107+
case st:
5108+
fail(f"Function {self.function.name} has an unsupported group configuration. (Unexpected state {st}.c)")
50885109
elif symbol == '/':
50895110
if self.positional_only:
50905111
fail("Function " + self.function.name + " uses '/' more than once.")
50915112
self.positional_only = True
5092-
# ps_required and ps_optional are allowed here, that allows positional-only without option groups
5113+
# REQUIRED and OPTIONAL are allowed here, that allows positional-only without option groups
50935114
# to work (and have default values!)
5094-
if (self.parameter_state not in (self.ps_required, self.ps_optional, self.ps_right_square_after, self.ps_group_before)) or self.group:
5095-
fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ".d)")
5115+
allowed = {
5116+
ParamState.REQUIRED,
5117+
ParamState.OPTIONAL,
5118+
ParamState.RIGHT_SQUARE_AFTER,
5119+
ParamState.GROUP_BEFORE,
5120+
}
5121+
if (self.parameter_state not in allowed) or self.group:
5122+
fail(f"Function {self.function.name} has an unsupported group configuration. (Unexpected state {self.parameter_state}.d)")
50965123
if self.keyword_only:
50975124
fail("Function " + self.function.name + " mixes keyword-only and positional-only parameters, which is unsupported.")
50985125
# fixup preceding parameters

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy