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 <[email protected]>
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

+78-51
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)