Repository navigation
[behave] Add stubs for model.Table and model.Row - #16721
Conversation
`behave.model` had no stubs at all, so `context.table` — the object almost every step implementation that uses a Gherkin data table touches — was `Incomplete`, and everything reached through it was `Any`. Adds `behave/model.pyi` covering `Table` and `Row`, and `behave/model_core.pyi` covering `Replayable`, which `Table` derives from. Both modules keep the module-level `__getattr__` escape hatch, so the rest of `behave.model` is unaffected and the package stays a partial stub. `Context` is wired up to the new types: - `table: Table | None`, assigned from `Step.table` in `Step.run`, and None at root - `active_outline: Row | None`, assigned from `scenario._row` in `ScenarioOutline.run` and reset to None afterwards Signatures were read off behave 1.3.3 and checked at runtime, including the cases that needed care: - `Table.__getitem__` is overloaded, since `table[0]` returns a `Row` while `table[0:2]` returns `list[Row]` - `Row.items()` returns a `zip` object, not a list or a view - `Row.as_dict()` returns an `OrderedDict` - `Row.get` is overloaded on its default, like `Mapping.get` - `Table.assert_equals` is `NoReturn`: it asserts, then unconditionally raises `NotImplementedError` - `Replayable.type` is `None` on the base class and a string on subclasses, so it is `ClassVar[str | None]` there and `ClassVar[str]` on `Table` - `Table.from_data` returns `Table` rather than `Self`, because one branch constructs `Table` directly instead of `cls` Verified with stubtest (84 modules, no issues) and `mypy --strict`: a representative step implementation that reads headings, iterates rows, and uses named, indexed and sliced access reported 14 `Any` errors before this change and none after.
This comment has been minimized.
This comment has been minimized.
|
Hii @srittau , requesting for a review and merge |
flake8-pyi Y067: an argument whose type is unknown and defaults to None is written as comments=None rather than comments: Incomplete | None = None. The class attribute keeps its annotation. Black then fits both signatures on one line.
This comment has been minimized.
This comment has been minimized.
srittau
left a comment
There was a problem hiding this comment.
Thanks, two comments below.
| @classmethod | ||
| def from_dicts(cls, headings: list[str], table_data: Sequence[Mapping[str, str]], line: int | None = None) -> Self: ... | ||
| @classmethod | ||
| def from_data( | ||
| cls, headings: list[str], table_data: Sequence[Mapping[str, str] | _RowData] | None = None, line: int = 0 | ||
| ) -> Table: ... |
There was a problem hiding this comment.
Mapping too narrow (there is an explicit isinstance check):
| @classmethod | |
| def from_dicts(cls, headings: list[str], table_data: Sequence[Mapping[str, str]], line: int | None = None) -> Self: ... | |
| @classmethod | |
| def from_data( | |
| cls, headings: list[str], table_data: Sequence[Mapping[str, str] | _RowData] | None = None, line: int = 0 | |
| ) -> Table: ... | |
| @classmethod | |
| def from_dicts(cls, headings: list[str], table_data: Sequence[dict[str, str]], line: int | None = None) -> Self: ... | |
| @classmethod | |
| def from_data( | |
| cls, headings: list[str], table_data: Sequence[dict[str, str] | _RowData] | None = None, line: int = 0 | |
| ) -> Table: ... |
There was a problem hiding this comment.
Done in a2f1e41 — both now take Sequence[dict[str, str]] / Sequence[dict[str, str] | _RowData].
from_dicts raises TypeError("%r (expected: List[dict]") when table_data[0] is not a dict, and from_data takes its else-branch and passes the data straight to Table(headings, rows=table_data), so a non-dict Mapping is rejected by one and silently mishandled by the other.
Left Row.from_dict on Mapping: it only does row_data.get(name), no isinstance check.
| from _typeshed import Incomplete | ||
| from collections import OrderedDict | ||
| from collections.abc import Iterable, Iterator, Mapping, Sequence | ||
| from typing import ClassVar, NoReturn, TypeAlias, TypeVar, overload |
There was a problem hiding this comment.
Use Never from typing_extensions. NoReturn is deprecated.
There was a problem hiding this comment.
Done in b7eb5c6 — Never from typing_extensions, and NoReturn is dropped from the typing import.
Both methods gate on isinstance(table_data[0], dict): from_dicts raises TypeError for a non-dict, and from_data silently takes the other branch and passes the data to Table(rows=...). A Mapping that is not a dict is not accepted by either, so annotate the element type as dict[str, str]. Row.from_dict keeps Mapping: it only calls row_data.get(name).
Table.assert_equals asserts and then raises NotImplementedError, so it never returns; Never is the non-deprecated spelling for that.
|
According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉 |
|
Hii @srittau , good for re-review |
behave.modelhad no stubs at all, socontext.table— the object almost every step implementation that uses a Gherkin data table touches — wasIncomplete, and everything reached through it wasAny:This adds
behave/model.pyicoveringTableandRow, plusbehave/model_core.pyicoveringReplayable, whichTablederives from. Both keep the module-level__getattr__escape hatch, so the rest ofbehave.modelis untouched and the package stays a partial stub.Contextis wired up to the new types:tableIncomplete | NoneTable | NoneStep.tableinStep.runactive_outlineIncompleteRow | Nonescenario._rowinScenarioOutline.runDetails worth a second look
Signatures were read off behave 1.3.3 and checked against the runtime. The cases that needed care:
Table.__getitem__is overloaded —table[0]gives aRow, buttable[0:2]giveslist[Row], since it delegates toself.rows[index].Row.items()returns azipobject, not a list or a dict view.Row.as_dict()returns anOrderedDict.Row.getis overloaded on its default, likeMapping.get.Table.assert_equalsisNoReturn— it asserts the comparison and then unconditionally raisesNotImplementedError.Replayable.typeisNoneon the base and a string on subclasses, so it isClassVar[str | None]there andClassVar[str]onTable.Table.from_datareturnsTable, notSelf— one branch constructsTabledirectly rather thancls.Row.from_dict's**kwargsare spelled out as the keyword-onlylineandcommentsthatRow.__init__actually accepts.Row.commentsstaysIncomplete | None: nothing in behave constructs aRowwith it, and there is noCommenttype in the package, so I did not want to guess.Validation
stubtest:Success: no issues found in 84 modulesmypy --strict: a representative step implementation readingheadings, iterating rows, and using named, indexed and sliced access reported 14Anyerrors before this change and none after. Everyassert_typeforstr,int,list[str],list[Row]andOrderedDict[str, str]now holds.