Add type annotations to LocaleScanner

Task-number: QTBUG-129566
Pick-to: 6.8
Change-Id: I768fda6b5202ebabc8283ecedead9157653862be
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Mate Barany 2024-10-07 16:38:39 +02:00
parent ba9d6b261b
commit b9e4f53b7e
2 changed files with 67 additions and 59 deletions

View File

@ -368,7 +368,7 @@ class CldrAccess (object):
for ignore, attrs in self.supplement('likelySubtags.xml').find('likelySubtags'): for ignore, attrs in self.supplement('likelySubtags.xml').find('likelySubtags'):
yield attrs['from'], attrs['to'] yield attrs['from'], attrs['to']
def numberSystem(self, system): def numberSystem(self, system: str) -> dict[str, str]:
"""Get a description of a numbering system. """Get a description of a numbering system.
Returns a mapping, with keys 'digits', 'type' and 'id'; the Returns a mapping, with keys 'digits', 'type' and 'id'; the

View File

@ -21,7 +21,7 @@ See individual classes for further detail.
from __future__ import annotations from __future__ import annotations
from localetools import Error from localetools import Error
from dateconverter import convert_date from dateconverter import convert_date
from typing import Any, Iterator from typing import Any, Callable, Iterator
from xml.dom import minidom from xml.dom import minidom
# The github version of CLDR uses '↑↑↑' to indicate "inherit" # The github version of CLDR uses '↑↑↑' to indicate "inherit"
@ -204,17 +204,17 @@ class Supplement (XmlScanner):
yield elt.nodeName, _attrsFromDom(elt) yield elt.nodeName, _attrsFromDom(elt)
class LocaleScanner (object): class LocaleScanner (object):
def __init__(self, name, nodes, root): def __init__(self, name: str, nodes: list[Node], root: XmlScanner) -> None:
"""Set up to scan data for a specified locale. """Set up to scan data for a specified locale.
First parameter is the name of the locale; it will be used in First parameter is the name of the locale; it will be used in
error messages. Second is a tuple of DOM root-nodes of files error messages. Second is a sequence of DOM root-nodes of files
with locale data, later ones serving as fall-backs for data with locale data, later ones serving as fall-backs for data
missing in earlier ones. Third parameter is the root locale's missing in earlier ones. Third parameter is the root locale's
DOM node.""" DOM node."""
self.name, self.nodes, self.base = name, nodes, root self.name, self.nodes, self.base = name, nodes, root
def find(self, xpath, default = None, draft = None): def find(self, xpath: str, default: str|None = None, draft: int|None = None) -> str:
"""XPath search for the content of an element. """XPath search for the content of an element.
Required argument, xpath, is the XPath to search for. Optional Required argument, xpath, is the XPath to search for. Optional
@ -234,15 +234,15 @@ class LocaleScanner (object):
raise raise
return default return default
def tagCodes(self): def tagCodes(self) -> Iterator[str]:
"""Yields four tag codes """Yields four tag codes
The tag codes are language, script, territory and variant; an The tag codes are language, script, territory and variant; an
empty value for any of them indicates that no value was empty value for any of them indicates that no value was
provided. The values are obtained from the primary file's provided. The values are obtained from the primary file's
top-level <identity> element.""" top-level <identity> element."""
root = self.nodes[0] root: Node = self.nodes[0]
ids = root.findUniqueChild('identity') ids: Node = root.findUniqueChild('identity')
for code in ('language', 'script', 'territory', 'variant'): for code in ('language', 'script', 'territory', 'variant'):
for node in ids.findAllChildren(code, allDull=True): for node in ids.findAllChildren(code, allDull=True):
try: try:
@ -254,7 +254,7 @@ class LocaleScanner (object):
else: # No value for this code, use empty else: # No value for this code, use empty
yield '' yield ''
def currencyData(self, isoCode): def currencyData(self, isoCode: str) -> Iterator[tuple[str, str]]:
"""Fetches currency data for this locale. """Fetches currency data for this locale.
Single argument, isoCode, is the ISO currency code for the Single argument, isoCode, is the ISO currency code for the
@ -262,30 +262,31 @@ class LocaleScanner (object):
includes some currency formats. includes some currency formats.
""" """
if isoCode: if isoCode:
stem = f'numbers/currencies/currency[{isoCode}]/' stem: str = f'numbers/currencies/currency[{isoCode}]/'
symbol = self.find(f'{stem}symbol', '') symbol: str = self.find(f'{stem}symbol', '')
name = self.__currencyDisplayName(stem) name: str = self.__currencyDisplayName(stem)
else: else:
symbol = name = '' symbol = name = ''
yield 'currencySymbol', symbol yield 'currencySymbol', symbol
yield 'currencyDisplayName', name yield 'currencyDisplayName', name
def numericData(self, lookup): def numericData(self, lookup: Callable[[str], dict[str, str]]
) -> Iterator[tuple[str, str]]:
"""Generate assorted numeric data for the locale. """Generate assorted numeric data for the locale.
First argument, lookup, is a callable that maps a numbering First argument, lookup, is a callable that maps a numbering
system's name to certain data about the system, as a mapping; system's name to certain data about the system, as a mapping;
we expect this to have 'digits' as a key. we expect this to have 'digits' as a key.
""" """
system = self.find('numbers/defaultNumberingSystem') system: str = self.find('numbers/defaultNumberingSystem')
stem = f'numbers/symbols[numberSystem={system}]/' stem: str = f'numbers/symbols[numberSystem={system}]/'
decimal = self.find(f'{stem}decimal') decimal: str = self.find(f'{stem}decimal')
group = self.find(f'{stem}group') group: str = self.find(f'{stem}group')
if decimal == group: if decimal == group:
# mn_Mong_MN @v43 :-( # mn_Mong_MN @v43 :-(
# https://unicode-org.atlassian.net/browse/CLDR-17883 # https://unicode-org.atlassian.net/browse/CLDR-17883
# https://unicode-org.atlassian.net/browse/CLDR-16413 # https://unicode-org.atlassian.net/browse/CLDR-16413
clean = Node.draftScore('approved') clean: int = Node.draftScore('approved')
decimal = self.find(f'{stem}decimal', draft=clean) decimal = self.find(f'{stem}decimal', draft=clean)
group = self.find(f'{stem}group', draft=clean) group = self.find(f'{stem}group', draft=clean)
assert decimal != group, (self.name, system, decimal) assert decimal != group, (self.name, system, decimal)
@ -297,9 +298,9 @@ class LocaleScanner (object):
yield 'exp', self.find(f'{stem}exponential') yield 'exp', self.find(f'{stem}exponential')
yield 'groupSizes', self.__numberGrouping(system) yield 'groupSizes', self.__numberGrouping(system)
digits = lookup(system)['digits'] digits: str = lookup(system)['digits']
assert len(digits) == 10 assert len(digits) == 10
zero = digits[0] zero: str = digits[0]
# Qt's number-formatting code assumes digits are consecutive # Qt's number-formatting code assumes digits are consecutive
# (except Suzhou - see QTBUG-85409 - which shares its zero # (except Suzhou - see QTBUG-85409 - which shares its zero
# with CLDR's very-non-contiguous hanidec): # with CLDR's very-non-contiguous hanidec):
@ -307,15 +308,15 @@ class LocaleScanner (object):
for i, c in enumerate(digits[1:], 1)) for i, c in enumerate(digits[1:], 1))
yield 'zero', zero yield 'zero', zero
plus = self.find(f'{stem}plusSign') plus: str = self.find(f'{stem}plusSign')
minus = self.find(f'{stem}minusSign') minus: str = self.find(f'{stem}minusSign')
yield 'plus', plus yield 'plus', plus
yield 'minus', minus yield 'minus', minus
# Currency formatting: # Currency formatting:
xpath = 'numbers/currencyFormats/currencyFormatLength/currencyFormat[accounting]/pattern' xpath = 'numbers/currencyFormats/currencyFormatLength/currencyFormat[accounting]/pattern'
try: try:
money = self.find(xpath.replace('Formats/', money: str = self.find(xpath.replace('Formats/',
f'Formats[numberSystem={system}]/')) f'Formats[numberSystem={system}]/'))
except Error: except Error:
money = self.find(xpath) money = self.find(xpath)
@ -327,7 +328,7 @@ class LocaleScanner (object):
neg = it neg = it
yield 'currencyNegativeFormat', neg yield 'currencyNegativeFormat', neg
def textPatternData(self): def textPatternData(self) -> Iterator[tuple[str, str]]:
for key in ('quotationStart', 'alternateQuotationEnd', for key in ('quotationStart', 'alternateQuotationEnd',
'quotationEnd', 'alternateQuotationStart'): 'quotationEnd', 'alternateQuotationStart'):
yield key, self.find(f'delimiters/{key}') yield key, self.find(f'delimiters/{key}')
@ -354,7 +355,8 @@ class LocaleScanner (object):
convert_date(self.find( convert_date(self.find(
f'{stem}{key}Formats/{key}FormatLength[{pair[1]}]/{key}Format/pattern'))) f'{stem}{key}Formats/{key}FormatLength[{pair[1]}]/{key}Format/pattern')))
def endonyms(self, language, script, territory, variant): def endonyms(self, language: str, script: str, territory: str, variant: str
) -> Iterator[tuple[str, str]]:
# TODO: take variant into account ? # TODO: take variant into account ?
# TODO: QTBUG-47892, support query for all combinations # TODO: QTBUG-47892, support query for all combinations
for seq in ((language, script, territory), for seq in ((language, script, territory),
@ -375,12 +377,12 @@ class LocaleScanner (object):
yield ('territoryEndonym', yield ('territoryEndonym',
self.find(f'localeDisplayNames/territories/territory[{territory}]', '')) self.find(f'localeDisplayNames/territories/territory[{territory}]', ''))
def unitData(self): def unitData(self) -> Iterator[tuple[str, str]]:
yield ('byte_unit', yield ('byte_unit',
self.find('units/unitLength[long]/unit[digital-byte]/displayName', self.find('units/unitLength[long]/unit[digital-byte]/displayName',
'bytes')) 'bytes'))
unit = self.__findUnit('', 'B') unit: str | None = self.__findUnit('', 'B')
cache = [] # Populated by the SI call, to give hints to the IEC call cache = [] # Populated by the SI call, to give hints to the IEC call
yield ('byte_si_quantified', yield ('byte_si_quantified',
';'.join(self.__unitCount('', unit, cache))) ';'.join(self.__unitCount('', unit, cache)))
@ -389,8 +391,8 @@ class LocaleScanner (object):
yield ('byte_iec_quantified', yield ('byte_iec_quantified',
';'.join(self.__unitCount('bi', 'iB', cache))) ';'.join(self.__unitCount('bi', 'iB', cache)))
def calendarNames(self, calendars): def calendarNames(self, calendars: list[str]) -> Iterator[tuple[str, str]]:
namings = self.__nameForms namings: tuple[tuple[str, str, str], ...] = self.__nameForms
for cal in calendars: for cal in calendars:
stem = f'dates/calendars/calendar[{cal}]/months/' stem = f'dates/calendars/calendar[{cal}]/months/'
for key, mode, size in namings: for key, mode, size in namings:
@ -408,7 +410,7 @@ class LocaleScanner (object):
';'.join(self.find(f'{stem}{prop}[{day}]') ';'.join(self.find(f'{stem}{prop}[{day}]')
for day in days)) for day in days))
def timeZoneNames(self, alias): def timeZoneNames(self, alias: dict[str, str]) -> Iterator[tuple[str, Any]]:
"""Names of zones and metazones for this locale. """Names of zones and metazones for this locale.
Single argument, alias, should be the first return from Single argument, alias, should be the first return from
@ -439,12 +441,12 @@ class LocaleScanner (object):
stem = 'dates/timeZoneNames' stem = 'dates/timeZoneNames'
# '+HH:mm;-HH:mm' (and :ss is also supported, but nowhere used in v45). # '+HH:mm;-HH:mm' (and :ss is also supported, but nowhere used in v45).
# Sometimes has single-digit hours # Sometimes has single-digit hours
hours = self.find(f'{stem}/hourFormat').split(';') hours: list[str] = self.find(f'{stem}/hourFormat').split(';')
assert all('H' in f and 'm' in f for f in hours), (hours, self.name) assert all('H' in f and 'm' in f for f in hours), (hours, self.name)
yield 'positiveOffsetFormat', hours[0] yield 'positiveOffsetFormat', hours[0]
yield 'negativeOffsetFormat', hours[1] yield 'negativeOffsetFormat', hours[1]
get = lambda n, s=stem: self.find(f'{s}/{n}').replace('{0}', '%0') get: Callable[[str], str] = lambda n, s=stem: self.find(f'{s}/{n}').replace('{0}', '%0')
# 'GMT{0}' with offset as {0} # 'GMT{0}' with offset as {0}
yield 'gmtOffsetFormat', get('gmtFormat') yield 'gmtOffsetFormat', get('gmtFormat')
# '{0} (Winter|Summer)? Time' with exemplar city or territory as {0} # '{0} (Winter|Summer)? Time' with exemplar city or territory as {0}
@ -455,14 +457,14 @@ class LocaleScanner (object):
# or territory as {0}. # or territory as {0}.
yield 'fallbackZoneFormat', get('fallbackFormat').replace('{1}', '%1') yield 'fallbackZoneFormat', get('fallbackFormat').replace('{1}', '%1')
zones = {} zones: dict[str, str|dict[str, str]] = {}
for elt in self.__find(f'{stem}/zone', allDull=True): for elt in self.__find(f'{stem}/zone', allDull=True):
iana = elt.attributes()['type'] iana: str = elt.attributes()['type']
if iana == 'Etc/Unknown': # CLDR special, of no interest to us if iana == 'Etc/Unknown': # CLDR special, of no interest to us
continue continue
# Map CLDR-canonical ID to IANA ID: # Map CLDR-canonical ID to IANA ID:
iana = alias.get(iana, iana) iana = alias.get(iana, iana)
data = zones.setdefault(iana, {}) data: dict[str, str|dict[str, str]] = zones.setdefault(iana, {})
for child in elt.dom.childNodes: for child in elt.dom.childNodes:
if child.nodeType != child.ELEMENT_NODE: if child.nodeType != child.ELEMENT_NODE:
@ -481,9 +483,9 @@ class LocaleScanner (object):
# Ignore any other child nodes. # Ignore any other child nodes.
yield 'zoneNaming', {k: self.__cleanZone(v) for k, v in zones.items() if v} yield 'zoneNaming', {k: self.__cleanZone(v) for k, v in zones.items() if v}
metazones = {} metazones: dict[str, dict[str, str]] = {}
for elt in self.__find(f'{stem}/metazone', allDull=True, mustFind=False): for elt in self.__find(f'{stem}/metazone', allDull=True, mustFind=False):
meta = elt.attributes()['type'] meta: str = elt.attributes()['type']
data = metazones.setdefault(meta, {}) data = metazones.setdefault(meta, {})
for child in elt.dom.childNodes: for child in elt.dom.childNodes:
if child.nodeType != child.ELEMENT_NODE: if child.nodeType != child.ELEMENT_NODE:
@ -504,11 +506,14 @@ class LocaleScanner (object):
('narrow', 'format', 'narrow'), ('narrow', 'format', 'narrow'),
) # Used for month and day names ) # Used for month and day names
def __find(self, xpath, allDull = False, mustFind = True): def __find(self, xpath: str, allDull: bool = False, mustFind: bool = True) -> Iterator[Node]:
retries = [ xpath.split('/') ] retries: list[list[str]] = [ xpath.split('/') ]
while retries: while retries:
tags, elts, roots = retries.pop(), self.nodes, (self.base.root,) tags: list[str] = retries.pop()
elts: tuple[Node, ...] = tuple(self.nodes)
roots: tuple[Node] = (self.base.root,)
for selector in tags: for selector in tags:
# tag is a str, attrs is a dict[str, str]
tag, attrs = _parseXPath(selector) tag, attrs = _parseXPath(selector)
elts = tuple(_iterateEach(e.findAllChildren(tag, attrs, elts = tuple(_iterateEach(e.findAllChildren(tag, attrs,
allDull=allDull) allDull=allDull)
@ -528,12 +533,13 @@ class LocaleScanner (object):
# is excessive (and alias only ever shows up in root.xml, # is excessive (and alias only ever shows up in root.xml,
# always with source="locale"). # always with source="locale").
for i, selector in enumerate(tags): for i, selector in enumerate(tags):
# tag is a str, attrs is a dict[str, str]
tag, attrs = _parseXPath(selector) tag, attrs = _parseXPath(selector)
for alias in tuple(_iterateEach(r.findAllChildren('alias', allDull=True) for alias in tuple(_iterateEach(r.findAllChildren('alias', allDull=True)
for r in roots)): for r in roots)):
assert alias.dom.attributes['source'].nodeValue == 'locale', alias assert alias.dom.attributes['source'].nodeValue == 'locale', alias
replace = alias.dom.attributes['path'].nodeValue.split('/') replace: list[str] = alias.dom.attributes['path'].nodeValue.split('/')
retries.append(self.__xpathJoin(tags[:i], replace, tags[i:])) retries.append(self.__xpathJoin(tags[:i], replace, tags[i:]))
roots = tuple(_iterateEach(r.findAllChildren(tag, attrs, roots = tuple(_iterateEach(r.findAllChildren(tag, attrs,
@ -562,7 +568,7 @@ class LocaleScanner (object):
raise Error(f'No {sought} in {self.name}') raise Error(f'No {sought} in {self.name}')
@staticmethod @staticmethod
def __skipInheritors(elts): def __skipInheritors(elts: tuple[Node, ...]) -> Iterator[Node]:
for elt in elts: for elt in elts:
try: try:
if elt.dom.firstChild.nodeValue != INHERIT: if elt.dom.firstChild.nodeValue != INHERIT:
@ -570,7 +576,7 @@ class LocaleScanner (object):
except (AttributeError, KeyError): except (AttributeError, KeyError):
yield elt yield elt
def __currencyDisplayName(self, stem): def __currencyDisplayName(self, stem: str) -> str | None:
try: try:
return self.find(stem + 'displayName') return self.find(stem + 'displayName')
except Error: except Error:
@ -583,7 +589,7 @@ class LocaleScanner (object):
return '' return ''
@staticmethod @staticmethod
def __zoneNames(dom, data): def __zoneNames(dom: minidom.Element, data: dict[str, str]) -> dict[str, str]:
for child in dom.childNodes: for child in dom.childNodes:
if child.nodeType != child.ELEMENT_NODE: if child.nodeType != child.ELEMENT_NODE:
continue continue
@ -598,7 +604,8 @@ class LocaleScanner (object):
return data return data
@staticmethod @staticmethod
def __cleanZone(data, keys = ('generic', 'standard', 'daylight')): def __cleanZone(data: dict[str, str|dict[str, str]], keys = ('generic', 'standard', 'daylight')
) -> dict[str, str|tuple[str|None, str|None, str|None]]:
if 'long' in data: if 'long' in data:
data['long'] = tuple(data['long'].get(k) for k in keys) data['long'] = tuple(data['long'].get(k) for k in keys)
if 'short' in data: if 'short' in data:
@ -606,7 +613,7 @@ class LocaleScanner (object):
# Leave any other keys alone. # Leave any other keys alone.
return data return data
def __findUnit(self, keySuffix, quantify, fallback=''): def __findUnit(self, keySuffix: str, quantify: str, fallback: str = '') -> str:
# The displayName for a quantified unit in en.xml is kByte # The displayName for a quantified unit in en.xml is kByte
# (even for unitLength[narrow]) instead of kB (etc.), so # (even for unitLength[narrow]) instead of kB (etc.), so
# prefer any unitPattern provided, but prune its placeholder: # prefer any unitPattern provided, but prune its placeholder:
@ -614,7 +621,7 @@ class LocaleScanner (object):
stem = f'units/unitLength[{size}{keySuffix}]/unit[digital-{quantify}byte]/' stem = f'units/unitLength[{size}{keySuffix}]/unit[digital-{quantify}byte]/'
for count in ('many', 'few', 'two', 'other', 'zero', 'one'): for count in ('many', 'few', 'two', 'other', 'zero', 'one'):
try: try:
ans = self.find(f'{stem}unitPattern[count={count}]') ans: str = self.find(f'{stem}unitPattern[count={count}]')
except Error: except Error:
continue continue
@ -633,10 +640,11 @@ class LocaleScanner (object):
return fallback return fallback
def __unitCount(self, keySuffix, suffix, cache, def __unitCount(self, keySuffix: str, suffix: str, cache: list[str],
# Stop at exa/exbi: 16 exbi = 2^{64} < zetta = # Stop at exa/exbi: 16 exbi = 2^{64} < zetta =
# 1000^7 < zebi = 2^{70}, the next quantifiers up: # 1000^7 < zebi = 2^{70}, the next quantifiers up:
siQuantifiers = ('kilo', 'mega', 'giga', 'tera', 'peta', 'exa')): siQuantifiers: tuple[str, ...] = ('kilo',
'mega', 'giga', 'tera', 'peta', 'exa')) -> Iterator[str]:
"""Work out the unit quantifiers. """Work out the unit quantifiers.
Unfortunately, the CLDR data only go up to terabytes and we Unfortunately, the CLDR data only go up to terabytes and we
@ -662,7 +670,7 @@ class LocaleScanner (object):
else: # first call else: # first call
tail = suffix = suffix or 'B' tail = suffix = suffix or 'B'
for q in siQuantifiers: for q in siQuantifiers:
it = self.__findUnit(keySuffix, q) it: str | None = self.__findUnit(keySuffix, q)
# kB for kilobyte, in contrast with KiB for IEC: # kB for kilobyte, in contrast with KiB for IEC:
q = q[0] if q == 'kilo' else q[0].upper() q = q[0] if q == 'kilo' else q[0].upper()
if not it: if not it:
@ -673,7 +681,7 @@ class LocaleScanner (object):
cache.append(rest) cache.append(rest)
yield it yield it
def __numberGrouping(self, system): def __numberGrouping(self, system: str) -> tuple[int, int, int]:
"""Sizes of groups of digits within a number. """Sizes of groups of digits within a number.
Returns a triple (least, higher, top) for which: Returns a triple (least, higher, top) for which:
@ -693,9 +701,9 @@ class LocaleScanner (object):
elsewhere).""" elsewhere)."""
top = int(self.find('numbers/minimumGroupingDigits')) top = int(self.find('numbers/minimumGroupingDigits'))
assert top < 4, top # We store it in a 2-bit field assert top < 4, top # We store it in a 2-bit field
grouping = self.find(f'numbers/decimalFormats[numberSystem={system}]/' grouping: str | None = self.find(f'numbers/decimalFormats[numberSystem={system}]/'
'decimalFormatLength/decimalFormat/pattern') 'decimalFormatLength/decimalFormat/pattern')
groups = grouping.split('.')[0].split(',')[-3:] groups: list[str] = grouping.split('.')[0].split(',')[-3:]
assert all(len(x) < 8 for x in groups[-2:]), grouping # we store them in 3-bit fields assert all(len(x) < 8 for x in groups[-2:]), grouping # we store them in 3-bit fields
if len(groups) > 2: if len(groups) > 2:
return len(groups[-1]), len(groups[-2]), top return len(groups[-1]), len(groups[-2]), top
@ -704,7 +712,7 @@ class LocaleScanner (object):
return size, size, top return size, size, top
@staticmethod @staticmethod
def __currencyFormats(patterns, plus, minus): def __currencyFormats(patterns: str, plus: str, minus: str) -> Iterator[str]:
for p in patterns.split(';'): for p in patterns.split(';'):
p = p.replace('0', '#').replace(',', '').replace('.', '') p = p.replace('0', '#').replace(',', '').replace('.', '')
try: try:
@ -725,17 +733,17 @@ class LocaleScanner (object):
yield p yield p
@staticmethod @staticmethod
def __fromLdmlListPattern(pattern): def __fromLdmlListPattern(pattern: str) -> str:
# This is a very limited parsing of the format for list pattern part only. # This is a very limited parsing of the format for list pattern part only.
return pattern.replace('{0}', '%1').replace('{1}', '%2').replace('{2}', '%3') return pattern.replace('{0}', '%1').replace('{1}', '%2').replace('{2}', '%3')
@staticmethod @staticmethod
def __fromLdmlPath(seq): # tool function for __xpathJoin() def __fromLdmlPath(seq: list[str]) -> Iterator[str]: # tool function for __xpathJoin()
"""Convert LDML's [@name='value'] to our [name=value] form.""" """Convert LDML's [@name='value'] to our [name=value] form."""
for it in seq: for it in seq:
# First dismember it: # First dismember it:
attrs = it.split('[') attrs: list[str] = it.split('[')
tag = attrs.pop(0) tag: str = attrs.pop(0)
if not attrs: # Short-cut the easy case: if not attrs: # Short-cut the easy case:
yield it yield it
continue continue
@ -752,7 +760,7 @@ class LocaleScanner (object):
yield '['.join(attrs) yield '['.join(attrs)
@classmethod @classmethod
def __xpathJoin(cls, head, insert, tail): def __xpathJoin(cls, head: list[str], insert: list[str], tail: list[str]) -> list[str]:
"""Join three lists of XPath selectors. """Join three lists of XPath selectors.
Each of head, insert and tail is a sequence of selectors but Each of head, insert and tail is a sequence of selectors but