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.
# fmt: off
from WebIDL import IDLUnionType
from WebIDL import IDLSequenceType
from collections import defaultdict
from itertools import groupby
from typing import Generator, Optional, cast
from typing import Optional
from collections.abc import Generator
from abc import abstractmethod
import operator
@ -2679,6 +2682,7 @@ def getAllTypes(
for d in descriptors:
for t in getTypesFromDescriptor(d):
if t.isRecord():
# pyrefly: ignore # missing-attribute
yield (t.inner, d)
yield (t, d)
for dictionary in dictionaries:
@ -4722,7 +4726,7 @@ pub(crate) fn init_{infoName}<D: DomTypes>() {{
assert isAlwaysInSlot or self.member.getExtendedAttribute("Cached")
isLazilyCachedInSlot = not isAlwaysInSlot
# 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
# CGUpdateMemberSlotsMethod, in the case when
# isAlwaysInSlot is true.

View file

@ -2,6 +2,10 @@
# 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/.
# fmt: off
from __future__ import annotations
import functools
import os
from typing import Any
@ -30,7 +34,7 @@ class Configuration:
enumConfig: dict[str, Any]
dictConfig: dict[str, Any]
unionConfig: dict[str, Any]
descriptors: list["Descriptor"]
descriptors: list[Descriptor]
interfaces: dict[str, IDLInterface]
def __init__(self, filename: str, parseData: list[IDLInterface]) -> None:
@ -86,46 +90,46 @@ class Configuration:
c.isCallback() and not c.isInterface()]
# Keep the descriptor list sorted for determinism.
def cmp(x, y):
def cmp(x: str, y: str) -> int:
return (x > y) - (x < y)
self.descriptors.sort(key=functools.cmp_to_key(lambda x, y: cmp(x.name, y.name)))
def getInterface(self, ifname: str) -> IDLInterface:
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."""
curr = self.descriptors
for key, val in filters.items():
if key == 'webIDLFile':
def getter(x: Descriptor):
def getter(x: Descriptor) -> str:
return x.interface.location.filename
elif key == 'hasInterfaceObject':
def getter(x):
def getter(x: Descriptor) -> bool:
return x.interface.hasInterfaceObject()
elif key == 'isCallback':
def getter(x):
def getter(x: Descriptor) -> bool:
return x.interface.isCallback()
elif key == 'isNamespace':
def getter(x):
def getter(x: Descriptor) -> bool:
return x.interface.isNamespace()
elif key == 'isJSImplemented':
def getter(x):
def getter(x: Descriptor) -> bool:
return x.interface.isJSImplemented()
elif key == 'isGlobal':
def getter(x):
def getter(x: Descriptor) -> bool:
return x.isGlobal()
elif key == 'isInline':
def getter(x) -> bool:
def getter(x: Descriptor) -> bool:
return x.interface.getExtendedAttribute('Inline') is not None
elif key == 'isExposedConditionally':
def getter(x):
def getter(x: Descriptor) -> bool:
return x.interface.isExposedConditionally()
elif key == 'isIteratorInterface':
def getter(x):
def getter(x: Descriptor) -> bool:
return x.interface.isIteratorInterface()
else:
def getter(x):
def getter(x: Descriptor) -> Any:
return getattr(x, key)
curr = [x for x in curr if getter(x) == val]
return curr
@ -159,7 +163,7 @@ class Configuration:
def getCallbacks(self, webIDLFile: str = "") -> list[IDLInterface]:
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.
"""
@ -172,7 +176,7 @@ class Configuration:
+ str(len(descriptors)) + " matches")
return descriptors[0]
def getDescriptorProvider(self) -> "DescriptorProvider":
def getDescriptorProvider(self) -> DescriptorProvider:
"""
Gets a descriptor provider that can provide descriptors as needed.
"""
@ -180,7 +184,7 @@ class Configuration:
class NoSuchDescriptorError(TypeError):
def __init__(self, str) -> None:
def __init__(self, str: str) -> None:
TypeError.__init__(self, str)
@ -188,10 +192,10 @@ class DescriptorProvider:
"""
A way of getting descriptors for interface names
"""
def __init__(self, config) -> None:
def __init__(self, config: Configuration) -> None:
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
context of the current descriptor.
@ -199,7 +203,7 @@ class DescriptorProvider:
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())
and not member.isStatic()
and (member.isLegacyUnforgeable()
@ -213,7 +217,7 @@ class Descriptor(DescriptorProvider):
interface: IDLInterface
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)
self.interface = interface
@ -289,7 +293,7 @@ class Descriptor(DescriptorProvider):
and any(MemberIsLegacyUnforgeable(m, self) for m in
self.interface.members))
self.operations = {
self.operations: dict[str, IDLMethod | None] = {
'IndexedGetter': None,
'IndexedSetter': None,
'IndexedDeleter': None,
@ -301,7 +305,7 @@ class Descriptor(DescriptorProvider):
self.hasDefaultToJSON = False
def addOperation(operation: str, m) -> None:
def addOperation(operation: str, m: IDLMethod) -> None:
if not self.operations[operation]:
self.operations[operation] = m
@ -320,7 +324,7 @@ class Descriptor(DescriptorProvider):
if not m.isMethod():
continue
def addIndexedOrNamedOperation(operation: str, m) -> None:
def addIndexedOrNamedOperation(operation: str, m: IDLMethod) -> None:
if not self.isGlobal():
self.proxy = True
if m.isIndexed():
@ -357,8 +361,8 @@ class Descriptor(DescriptorProvider):
# array of extended attributes.
self.extendedAttributes = {'all': {}, 'getterOnly': {}, 'setterOnly': {}}
def addExtendedAttribute(attribute, config) -> None:
def add(key: str, members, attribute) -> None:
def addExtendedAttribute(attribute: dict[str, Any], config: Configuration) -> None:
def add(key: str, members: list[str], attribute: dict[str, Any]) -> None:
for member in members:
self.extendedAttributes[key].setdefault(member, []).append(attribute)
@ -405,7 +409,7 @@ class Descriptor(DescriptorProvider):
config.maxProtoChainLength = max(config.maxProtoChainLength,
len(self.prototypeChain))
def maybeGetSuperModule(self):
def maybeGetSuperModule(self) -> str | None:
"""
Returns name of super module if self is part of it
"""
@ -416,10 +420,10 @@ class Descriptor(DescriptorProvider):
return filename
return None
def binaryNameFor(self, name: str, isStatic: bool):
def binaryNameFor(self, name: str, isStatic: bool) -> str:
return self._binaryNames.get((name, isStatic), name)
def internalNameFor(self, name: str):
def internalNameFor(self, name: str) -> str:
return self._internalNames.get(name, name)
def hasNamedPropertiesObject(self) -> bool:
@ -431,8 +435,8 @@ class Descriptor(DescriptorProvider):
def supportsNamedProperties(self) -> bool:
return self.operations['NamedGetter'] is not None
def getExtendedAttributes(self, member, getter=False, setter=False):
def maybeAppendInfallibleToAttrs(attrs, throws) -> None:
def getExtendedAttributes(self, member: IDLMethod, getter: bool = False, setter: bool = False) -> list[str]:
def maybeAppendInfallibleToAttrs(attrs: list[str], throws: bool | None) -> None:
if throws is None:
attrs.append("infallible")
elif throws is True:
@ -458,7 +462,7 @@ class Descriptor(DescriptorProvider):
maybeAppendInfallibleToAttrs(attrs, throws)
return attrs
def getParentName(self):
def getParentName(self) -> str | None:
parent = self.interface.parent
while parent:
if not parent.getExtendedAttribute("Inline"):
@ -469,30 +473,30 @@ class Descriptor(DescriptorProvider):
def supportsIndexedProperties(self) -> bool:
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
# that's not a cross-origin object. The WindowProxy is.
return self.concrete and self.interface.hasCrossOriginMembers and not self.isGlobal()
def hasDescendants(self):
def hasDescendants(self) -> bool:
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()
return ctor and ctor.isHTMLConstructor()
return (ctor and ctor.isHTMLConstructor()) or False
def shouldHaveGetConstructorObjectMethod(self):
def shouldHaveGetConstructorObjectMethod(self) -> bool:
assert self.interface.hasInterfaceObject()
if self.interface.getExtendedAttribute("Inline"):
return False
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()
def isExposedConditionally(self):
def isExposedConditionally(self) -> bool:
return self.interface.isExposedConditionally()
def isGlobal(self) -> bool:
@ -507,19 +511,19 @@ class Descriptor(DescriptorProvider):
# Some utility methods
def MakeNativeName(name):
def MakeNativeName(name: str) -> str:
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]
def getModuleFromObject(object: IDLObject):
def getModuleFromObject(object: IDLObject) -> str:
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
"""
@ -570,7 +574,7 @@ def getUnwrappedType(type: IDLType) -> IDLType:
return type
def iteratorNativeType(descriptor: Descriptor, infer: bool = False):
def iteratorNativeType(descriptor: Descriptor, infer: bool = False) -> str:
assert descriptor.interface.maplikeOrSetlikeOrIterable is not None
iterableDecl = descriptor.interface.maplikeOrSetlikeOrIterable
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
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# fmt: off
from __future__ import annotations
import os
import sys
import json
import re
from typing import TYPE_CHECKING
from collections.abc import Iterator
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
SERVO_ROOT = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..", ".."))
FILTER_PATTERN = re.compile("// skip-unless ([A-Z_]+)\n")
if TYPE_CHECKING:
from configuration import Configuration
from WebIDL import Parser
def main() -> None:
os.chdir(os.path.join(os.path.dirname(__file__)))
@ -84,13 +93,13 @@ def main() -> None:
f.write(module.encode("utf-8"))
def make_dir(path: str):
def make_dir(path: str)-> str:
if not os.path.exists(path):
os.makedirs(path)
return path
def generate(config, name: str, filename: str) -> None:
def generate(config: Configuration, name: str, filename: str) -> None:
from codegen import GlobalGenRoots
root = getattr(GlobalGenRoots, name)(config)
code = root.define()
@ -98,7 +107,7 @@ def generate(config, name: str, filename: str) -> None:
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:
"""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
@ -132,7 +141,7 @@ def add_css_properties_attributes(css_properties_json: str, parser) -> None:
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
if property_name != "float":
yield property_name
@ -149,7 +158,7 @@ def attribute_names(property_name: str):
# 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:
chars = chars[1:]
next_is_uppercase = False

View file

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