script_bindings(python): Add ruff rule for strict typing in function (#38694)

This is part of introducing the Ruff rule [flake8-annotations
(ANN)](https://docs.astral.sh/ruff/rules/#flake8-annotations-ann) into
the script_bindings folder.

Since `codegen.py` has 10k lines of code, the strategy is to introduce
the rule first while ignoring the `ANN` Ruff rule for `codegen.py`. We
will then gradually add type annotations slice by slice to ensure no new
errors occur outside of `ANN`.

Testing: `./mach test-wpt webidl`

---------

Signed-off-by: Jerens Lensun <jerensslensun@gmail.com>
This commit is contained in:
Jerens Lensun 2025-08-18 15:26:30 +08:00 committed by GitHub
parent 27dededa65
commit 788d6db94d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 73 additions and 54 deletions

View file

@ -4,11 +4,14 @@
# Common codegen classes. # Common codegen classes.
# fmt: off
from WebIDL import IDLUnionType from WebIDL import IDLUnionType
from WebIDL import IDLSequenceType from WebIDL import IDLSequenceType
from collections import defaultdict from collections import defaultdict
from itertools import groupby from itertools import groupby
from typing import Generator, Optional, cast from typing import Optional
from collections.abc import Generator
from abc import abstractmethod from abc import abstractmethod
import operator import operator
@ -2679,6 +2682,7 @@ def getAllTypes(
for d in descriptors: for d in descriptors:
for t in getTypesFromDescriptor(d): for t in getTypesFromDescriptor(d):
if t.isRecord(): if t.isRecord():
# pyrefly: ignore # missing-attribute
yield (t.inner, d) yield (t.inner, d)
yield (t, d) yield (t, d)
for dictionary in dictionaries: for dictionary in dictionaries:
@ -4722,7 +4726,7 @@ pub(crate) fn init_{infoName}<D: DomTypes>() {{
assert isAlwaysInSlot or self.member.getExtendedAttribute("Cached") assert isAlwaysInSlot or self.member.getExtendedAttribute("Cached")
isLazilyCachedInSlot = not isAlwaysInSlot isLazilyCachedInSlot = not isAlwaysInSlot
# pyrefly: ignore # unknown-name # pyrefly: ignore # unknown-name
slotIndex = memberReservedSlot(self.member) # noqa:FIXME: memberReservedSlot is not defined slotIndex = memberReservedSlot(self.member) # noqa: F821 FIXME: memberReservedSlot is not defined
# We'll statically assert that this is not too big in # We'll statically assert that this is not too big in
# CGUpdateMemberSlotsMethod, in the case when # CGUpdateMemberSlotsMethod, in the case when
# isAlwaysInSlot is true. # isAlwaysInSlot is true.

View file

@ -2,6 +2,10 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
# fmt: off
from __future__ import annotations
import functools import functools
import os import os
from typing import Any from typing import Any
@ -30,7 +34,7 @@ class Configuration:
enumConfig: dict[str, Any] enumConfig: dict[str, Any]
dictConfig: dict[str, Any] dictConfig: dict[str, Any]
unionConfig: dict[str, Any] unionConfig: dict[str, Any]
descriptors: list["Descriptor"] descriptors: list[Descriptor]
interfaces: dict[str, IDLInterface] interfaces: dict[str, IDLInterface]
def __init__(self, filename: str, parseData: list[IDLInterface]) -> None: def __init__(self, filename: str, parseData: list[IDLInterface]) -> None:
@ -86,46 +90,46 @@ class Configuration:
c.isCallback() and not c.isInterface()] c.isCallback() and not c.isInterface()]
# Keep the descriptor list sorted for determinism. # Keep the descriptor list sorted for determinism.
def cmp(x, y): def cmp(x: str, y: str) -> int:
return (x > y) - (x < y) return (x > y) - (x < y)
self.descriptors.sort(key=functools.cmp_to_key(lambda x, y: cmp(x.name, y.name))) self.descriptors.sort(key=functools.cmp_to_key(lambda x, y: cmp(x.name, y.name)))
def getInterface(self, ifname: str) -> IDLInterface: def getInterface(self, ifname: str) -> IDLInterface:
return self.interfaces[ifname] return self.interfaces[ifname]
def getDescriptors(self, **filters) -> list["Descriptor"]: def getDescriptors(self, **filters: IDLInterface) -> list[Descriptor]:
"""Gets the descriptors that match the given filters.""" """Gets the descriptors that match the given filters."""
curr = self.descriptors curr = self.descriptors
for key, val in filters.items(): for key, val in filters.items():
if key == 'webIDLFile': if key == 'webIDLFile':
def getter(x: Descriptor): def getter(x: Descriptor) -> str:
return x.interface.location.filename return x.interface.location.filename
elif key == 'hasInterfaceObject': elif key == 'hasInterfaceObject':
def getter(x): def getter(x: Descriptor) -> bool:
return x.interface.hasInterfaceObject() return x.interface.hasInterfaceObject()
elif key == 'isCallback': elif key == 'isCallback':
def getter(x): def getter(x: Descriptor) -> bool:
return x.interface.isCallback() return x.interface.isCallback()
elif key == 'isNamespace': elif key == 'isNamespace':
def getter(x): def getter(x: Descriptor) -> bool:
return x.interface.isNamespace() return x.interface.isNamespace()
elif key == 'isJSImplemented': elif key == 'isJSImplemented':
def getter(x): def getter(x: Descriptor) -> bool:
return x.interface.isJSImplemented() return x.interface.isJSImplemented()
elif key == 'isGlobal': elif key == 'isGlobal':
def getter(x): def getter(x: Descriptor) -> bool:
return x.isGlobal() return x.isGlobal()
elif key == 'isInline': elif key == 'isInline':
def getter(x) -> bool: def getter(x: Descriptor) -> bool:
return x.interface.getExtendedAttribute('Inline') is not None return x.interface.getExtendedAttribute('Inline') is not None
elif key == 'isExposedConditionally': elif key == 'isExposedConditionally':
def getter(x): def getter(x: Descriptor) -> bool:
return x.interface.isExposedConditionally() return x.interface.isExposedConditionally()
elif key == 'isIteratorInterface': elif key == 'isIteratorInterface':
def getter(x): def getter(x: Descriptor) -> bool:
return x.interface.isIteratorInterface() return x.interface.isIteratorInterface()
else: else:
def getter(x): def getter(x: Descriptor) -> Any:
return getattr(x, key) return getattr(x, key)
curr = [x for x in curr if getter(x) == val] curr = [x for x in curr if getter(x) == val]
return curr return curr
@ -159,7 +163,7 @@ class Configuration:
def getCallbacks(self, webIDLFile: str = "") -> list[IDLInterface]: def getCallbacks(self, webIDLFile: str = "") -> list[IDLInterface]:
return self._filterForFile(self.callbacks, webIDLFile=webIDLFile) return self._filterForFile(self.callbacks, webIDLFile=webIDLFile)
def getDescriptor(self, interfaceName: str) -> "Descriptor": def getDescriptor(self, interfaceName: str) -> Descriptor:
""" """
Gets the appropriate descriptor for the given interface name. Gets the appropriate descriptor for the given interface name.
""" """
@ -172,7 +176,7 @@ class Configuration:
+ str(len(descriptors)) + " matches") + str(len(descriptors)) + " matches")
return descriptors[0] return descriptors[0]
def getDescriptorProvider(self) -> "DescriptorProvider": def getDescriptorProvider(self) -> DescriptorProvider:
""" """
Gets a descriptor provider that can provide descriptors as needed. Gets a descriptor provider that can provide descriptors as needed.
""" """
@ -180,7 +184,7 @@ class Configuration:
class NoSuchDescriptorError(TypeError): class NoSuchDescriptorError(TypeError):
def __init__(self, str) -> None: def __init__(self, str: str) -> None:
TypeError.__init__(self, str) TypeError.__init__(self, str)
@ -188,10 +192,10 @@ class DescriptorProvider:
""" """
A way of getting descriptors for interface names A way of getting descriptors for interface names
""" """
def __init__(self, config) -> None: def __init__(self, config: Configuration) -> None:
self.config = config self.config = config
def getDescriptor(self, interfaceName: str) -> "Descriptor": def getDescriptor(self, interfaceName: str) -> Descriptor:
""" """
Gets the appropriate descriptor for the given interface name given the Gets the appropriate descriptor for the given interface name given the
context of the current descriptor. context of the current descriptor.
@ -199,7 +203,7 @@ class DescriptorProvider:
return self.config.getDescriptor(interfaceName) return self.config.getDescriptor(interfaceName)
def MemberIsLegacyUnforgeable(member: IDLAttribute | IDLMethod, descriptor: "Descriptor") -> bool: def MemberIsLegacyUnforgeable(member: IDLAttribute | IDLMethod, descriptor: Descriptor) -> bool:
return ((member.isAttr() or member.isMethod()) return ((member.isAttr() or member.isMethod())
and not member.isStatic() and not member.isStatic()
and (member.isLegacyUnforgeable() and (member.isLegacyUnforgeable()
@ -213,7 +217,7 @@ class Descriptor(DescriptorProvider):
interface: IDLInterface interface: IDLInterface
uniqueImplementation: bool uniqueImplementation: bool
def __init__(self, config, interface: IDLInterface, desc: dict[str, Any]) -> None: def __init__(self, config: Configuration, interface: IDLInterface, desc: dict[str, Any]) -> None:
DescriptorProvider.__init__(self, config) DescriptorProvider.__init__(self, config)
self.interface = interface self.interface = interface
@ -289,7 +293,7 @@ class Descriptor(DescriptorProvider):
and any(MemberIsLegacyUnforgeable(m, self) for m in and any(MemberIsLegacyUnforgeable(m, self) for m in
self.interface.members)) self.interface.members))
self.operations = { self.operations: dict[str, IDLMethod | None] = {
'IndexedGetter': None, 'IndexedGetter': None,
'IndexedSetter': None, 'IndexedSetter': None,
'IndexedDeleter': None, 'IndexedDeleter': None,
@ -301,7 +305,7 @@ class Descriptor(DescriptorProvider):
self.hasDefaultToJSON = False self.hasDefaultToJSON = False
def addOperation(operation: str, m) -> None: def addOperation(operation: str, m: IDLMethod) -> None:
if not self.operations[operation]: if not self.operations[operation]:
self.operations[operation] = m self.operations[operation] = m
@ -320,7 +324,7 @@ class Descriptor(DescriptorProvider):
if not m.isMethod(): if not m.isMethod():
continue continue
def addIndexedOrNamedOperation(operation: str, m) -> None: def addIndexedOrNamedOperation(operation: str, m: IDLMethod) -> None:
if not self.isGlobal(): if not self.isGlobal():
self.proxy = True self.proxy = True
if m.isIndexed(): if m.isIndexed():
@ -357,8 +361,8 @@ class Descriptor(DescriptorProvider):
# array of extended attributes. # array of extended attributes.
self.extendedAttributes = {'all': {}, 'getterOnly': {}, 'setterOnly': {}} self.extendedAttributes = {'all': {}, 'getterOnly': {}, 'setterOnly': {}}
def addExtendedAttribute(attribute, config) -> None: def addExtendedAttribute(attribute: dict[str, Any], config: Configuration) -> None:
def add(key: str, members, attribute) -> None: def add(key: str, members: list[str], attribute: dict[str, Any]) -> None:
for member in members: for member in members:
self.extendedAttributes[key].setdefault(member, []).append(attribute) self.extendedAttributes[key].setdefault(member, []).append(attribute)
@ -405,7 +409,7 @@ class Descriptor(DescriptorProvider):
config.maxProtoChainLength = max(config.maxProtoChainLength, config.maxProtoChainLength = max(config.maxProtoChainLength,
len(self.prototypeChain)) len(self.prototypeChain))
def maybeGetSuperModule(self): def maybeGetSuperModule(self) -> str | None:
""" """
Returns name of super module if self is part of it Returns name of super module if self is part of it
""" """
@ -416,10 +420,10 @@ class Descriptor(DescriptorProvider):
return filename return filename
return None return None
def binaryNameFor(self, name: str, isStatic: bool): def binaryNameFor(self, name: str, isStatic: bool) -> str:
return self._binaryNames.get((name, isStatic), name) return self._binaryNames.get((name, isStatic), name)
def internalNameFor(self, name: str): def internalNameFor(self, name: str) -> str:
return self._internalNames.get(name, name) return self._internalNames.get(name, name)
def hasNamedPropertiesObject(self) -> bool: def hasNamedPropertiesObject(self) -> bool:
@ -431,8 +435,8 @@ class Descriptor(DescriptorProvider):
def supportsNamedProperties(self) -> bool: def supportsNamedProperties(self) -> bool:
return self.operations['NamedGetter'] is not None return self.operations['NamedGetter'] is not None
def getExtendedAttributes(self, member, getter=False, setter=False): def getExtendedAttributes(self, member: IDLMethod, getter: bool = False, setter: bool = False) -> list[str]:
def maybeAppendInfallibleToAttrs(attrs, throws) -> None: def maybeAppendInfallibleToAttrs(attrs: list[str], throws: bool | None) -> None:
if throws is None: if throws is None:
attrs.append("infallible") attrs.append("infallible")
elif throws is True: elif throws is True:
@ -458,7 +462,7 @@ class Descriptor(DescriptorProvider):
maybeAppendInfallibleToAttrs(attrs, throws) maybeAppendInfallibleToAttrs(attrs, throws)
return attrs return attrs
def getParentName(self): def getParentName(self) -> str | None:
parent = self.interface.parent parent = self.interface.parent
while parent: while parent:
if not parent.getExtendedAttribute("Inline"): if not parent.getExtendedAttribute("Inline"):
@ -469,30 +473,30 @@ class Descriptor(DescriptorProvider):
def supportsIndexedProperties(self) -> bool: def supportsIndexedProperties(self) -> bool:
return self.operations['IndexedGetter'] is not None return self.operations['IndexedGetter'] is not None
def isMaybeCrossOriginObject(self): def isMaybeCrossOriginObject(self) -> bool:
# If we're isGlobal and have cross-origin members, we're a Window, and # If we're isGlobal and have cross-origin members, we're a Window, and
# that's not a cross-origin object. The WindowProxy is. # that's not a cross-origin object. The WindowProxy is.
return self.concrete and self.interface.hasCrossOriginMembers and not self.isGlobal() return self.concrete and self.interface.hasCrossOriginMembers and not self.isGlobal()
def hasDescendants(self): def hasDescendants(self) -> bool:
return (self.interface.getUserData("hasConcreteDescendant", False) return (self.interface.getUserData("hasConcreteDescendant", False)
or self.interface.getUserData("hasProxyDescendant", False)) or self.interface.getUserData("hasProxyDescendant", False) or False)
def hasHTMLConstructor(self): def hasHTMLConstructor(self) -> bool:
ctor = self.interface.ctor() ctor = self.interface.ctor()
return ctor and ctor.isHTMLConstructor() return (ctor and ctor.isHTMLConstructor()) or False
def shouldHaveGetConstructorObjectMethod(self): def shouldHaveGetConstructorObjectMethod(self) -> bool:
assert self.interface.hasInterfaceObject() assert self.interface.hasInterfaceObject()
if self.interface.getExtendedAttribute("Inline"): if self.interface.getExtendedAttribute("Inline"):
return False return False
return (self.interface.isCallback() or self.interface.isNamespace() return (self.interface.isCallback() or self.interface.isNamespace()
or self.hasDescendants() or self.hasHTMLConstructor()) or self.hasDescendants() or self.hasHTMLConstructor() or False)
def shouldCacheConstructor(self): def shouldCacheConstructor(self) -> bool:
return self.hasDescendants() or self.hasHTMLConstructor() return self.hasDescendants() or self.hasHTMLConstructor()
def isExposedConditionally(self): def isExposedConditionally(self) -> bool:
return self.interface.isExposedConditionally() return self.interface.isExposedConditionally()
def isGlobal(self) -> bool: def isGlobal(self) -> bool:
@ -507,19 +511,19 @@ class Descriptor(DescriptorProvider):
# Some utility methods # Some utility methods
def MakeNativeName(name): def MakeNativeName(name: str) -> str:
return name[0].upper() + name[1:] return name[0].upper() + name[1:]
def getIdlFileName(object: IDLObject): def getIdlFileName(object: IDLObject) -> str:
return os.path.basename(object.location.filename).split('.webidl')[0] return os.path.basename(object.location.filename).split('.webidl')[0]
def getModuleFromObject(object: IDLObject): def getModuleFromObject(object: IDLObject) -> str:
return ('crate::codegen::GenericBindings::' + getIdlFileName(object) + 'Binding') return ('crate::codegen::GenericBindings::' + getIdlFileName(object) + 'Binding')
def getTypesFromDescriptor(descriptor: Descriptor): def getTypesFromDescriptor(descriptor: Descriptor) -> list[IDLType]:
""" """
Get all argument and return types for all members of the descriptor Get all argument and return types for all members of the descriptor
""" """
@ -570,7 +574,7 @@ def getUnwrappedType(type: IDLType) -> IDLType:
return type return type
def iteratorNativeType(descriptor: Descriptor, infer: bool = False): def iteratorNativeType(descriptor: Descriptor, infer: bool = False) -> str:
assert descriptor.interface.maplikeOrSetlikeOrIterable is not None assert descriptor.interface.maplikeOrSetlikeOrIterable is not None
iterableDecl = descriptor.interface.maplikeOrSetlikeOrIterable iterableDecl = descriptor.interface.maplikeOrSetlikeOrIterable
assert (iterableDecl.isIterable() and iterableDecl.isPairIterator()) \ assert (iterableDecl.isIterable() and iterableDecl.isPairIterator()) \

View file

@ -2,16 +2,25 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
# fmt: off
from __future__ import annotations
import os import os
import sys import sys
import json import json
import re import re
from typing import TYPE_CHECKING
from collections.abc import Iterator
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
SERVO_ROOT = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..", "..")) SERVO_ROOT = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..", ".."))
FILTER_PATTERN = re.compile("// skip-unless ([A-Z_]+)\n") FILTER_PATTERN = re.compile("// skip-unless ([A-Z_]+)\n")
if TYPE_CHECKING:
from configuration import Configuration
from WebIDL import Parser
def main() -> None: def main() -> None:
os.chdir(os.path.join(os.path.dirname(__file__))) os.chdir(os.path.join(os.path.dirname(__file__)))
@ -84,13 +93,13 @@ def main() -> None:
f.write(module.encode("utf-8")) f.write(module.encode("utf-8"))
def make_dir(path: str): def make_dir(path: str)-> str:
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
return path return path
def generate(config, name: str, filename: str) -> None: def generate(config: Configuration, name: str, filename: str) -> None:
from codegen import GlobalGenRoots from codegen import GlobalGenRoots
root = getattr(GlobalGenRoots, name)(config) root = getattr(GlobalGenRoots, name)(config)
code = root.define() code = root.define()
@ -98,7 +107,7 @@ def generate(config, name: str, filename: str) -> None:
f.write(code.encode("utf-8")) f.write(code.encode("utf-8"))
def add_css_properties_attributes(css_properties_json: str, parser) -> None: def add_css_properties_attributes(css_properties_json: str, parser: Parser) -> None:
def map_preference_name(preference_name: str) -> str: def map_preference_name(preference_name: str) -> str:
"""Map between Stylo preference names and Servo preference names as the """Map between Stylo preference names and Servo preference names as the
`css-properties.json` file is generated by Stylo. This should be kept in sync with the `css-properties.json` file is generated by Stylo. This should be kept in sync with the
@ -132,7 +141,7 @@ def add_css_properties_attributes(css_properties_json: str, parser) -> None:
parser.parse(idl, "CSSStyleDeclaration_generated.webidl") parser.parse(idl, "CSSStyleDeclaration_generated.webidl")
def attribute_names(property_name: str): def attribute_names(property_name: str) -> Iterator[str]:
# https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-dashed-attribute # https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-dashed-attribute
if property_name != "float": if property_name != "float":
yield property_name yield property_name
@ -149,7 +158,7 @@ def attribute_names(property_name: str):
# https://drafts.csswg.org/cssom/#css-property-to-idl-attribute # https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
def camel_case(chars: str, webkit_prefixed: bool = False): def camel_case(chars: str, webkit_prefixed: bool = False) -> Iterator[str]:
if webkit_prefixed: if webkit_prefixed:
chars = chars[1:] chars = chars[1:]
next_is_uppercase = False next_is_uppercase = False

View file

@ -8,7 +8,8 @@ extend-exclude = [
# upstream # upstream
"third_party/**", "third_party/**",
"python/mach/**", "python/mach/**",
"components/**", "components/net/**",
"components/shared/**",
"tests/**", "tests/**",
] ]
@ -30,7 +31,8 @@ ignore = [
] ]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"!python/**/**.py" = ["ANN"] "etc/**" = ["ANN"]
"components/script_bindings/codegen/codegen.py" = ["ANN"]
"**/test.py" = ["ANN"] "**/test.py" = ["ANN"]
"**/*_tests.py" = ["ANN"] "**/*_tests.py" = ["ANN"]
"**/tests/**/*.py" = ["ANN"] "**/tests/**/*.py" = ["ANN"]