You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-12-02 16:48:55 +00:00
SCons: Integrate annotations where relevant
• Expand Ruff linter to catch & upgrade legacy type-hint syntax
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from misc.utility.scons_hints import *
|
from misc.utility.scons_hints import *
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import profiling_builders
|
import profiling_builders
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ Import("env")
|
|||||||
env.add_source_files(env.core_sources, "*.cpp")
|
env.add_source_files(env.core_sources, "*.cpp")
|
||||||
|
|
||||||
|
|
||||||
def get_profiler_and_path_from_path(path: pathlib.Path) -> Tuple[str, pathlib.Path]:
|
def get_profiler_and_path_from_path(path: pathlib.Path) -> tuple[str, pathlib.Path]:
|
||||||
if not path.is_dir():
|
if not path.is_dir():
|
||||||
print("profiler_path must be empty or point to a directory.")
|
print("profiler_path must be empty or point to a directory.")
|
||||||
Exit(255)
|
Exit(255)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import math
|
import math
|
||||||
@@ -6,7 +7,6 @@ import os
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Dict, List, Set
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ class ClassStatusProgress:
|
|||||||
self.described: int = described
|
self.described: int = described
|
||||||
self.total: int = total
|
self.total: int = total
|
||||||
|
|
||||||
def __add__(self, other: "ClassStatusProgress"):
|
def __add__(self, other: ClassStatusProgress):
|
||||||
return ClassStatusProgress(self.described + other.described, self.total + other.total)
|
return ClassStatusProgress(self.described + other.described, self.total + other.total)
|
||||||
|
|
||||||
def increment(self, described: bool):
|
def increment(self, described: bool):
|
||||||
@@ -178,7 +178,7 @@ class ClassStatus:
|
|||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.has_brief_description: bool = True
|
self.has_brief_description: bool = True
|
||||||
self.has_description: bool = True
|
self.has_description: bool = True
|
||||||
self.progresses: Dict[str, ClassStatusProgress] = {
|
self.progresses: dict[str, ClassStatusProgress] = {
|
||||||
"methods": ClassStatusProgress(),
|
"methods": ClassStatusProgress(),
|
||||||
"constants": ClassStatusProgress(),
|
"constants": ClassStatusProgress(),
|
||||||
"members": ClassStatusProgress(),
|
"members": ClassStatusProgress(),
|
||||||
@@ -188,7 +188,7 @@ class ClassStatus:
|
|||||||
"constructors": ClassStatusProgress(),
|
"constructors": ClassStatusProgress(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __add__(self, other: "ClassStatus"):
|
def __add__(self, other: ClassStatus):
|
||||||
new_status = ClassStatus()
|
new_status = ClassStatus()
|
||||||
new_status.name = self.name
|
new_status.name = self.name
|
||||||
new_status.has_brief_description = self.has_brief_description and other.has_brief_description
|
new_status.has_brief_description = self.has_brief_description and other.has_brief_description
|
||||||
@@ -213,8 +213,8 @@ class ClassStatus:
|
|||||||
sum += self.progresses[k].total
|
sum += self.progresses[k].total
|
||||||
return sum < 1
|
return sum < 1
|
||||||
|
|
||||||
def make_output(self) -> Dict[str, str]:
|
def make_output(self) -> dict[str, str]:
|
||||||
output: Dict[str, str] = {}
|
output: dict[str, str] = {}
|
||||||
output["name"] = color("name", self.name)
|
output["name"] = color("name", self.name)
|
||||||
|
|
||||||
ok_string = color("part_good", "OK")
|
ok_string = color("part_good", "OK")
|
||||||
@@ -295,8 +295,8 @@ class ClassStatus:
|
|||||||
# Arguments #
|
# Arguments #
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
input_file_list: List[str] = []
|
input_file_list: list[str] = []
|
||||||
input_class_list: List[str] = []
|
input_class_list: list[str] = []
|
||||||
merged_file: str = ""
|
merged_file: str = ""
|
||||||
|
|
||||||
for arg in sys.argv[1:]:
|
for arg in sys.argv[1:]:
|
||||||
@@ -372,8 +372,8 @@ if len(input_file_list) < 1 or flags["h"]:
|
|||||||
# Parse class list #
|
# Parse class list #
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
class_names: List[str] = []
|
class_names: list[str] = []
|
||||||
classes: Dict[str, ET.Element] = {}
|
classes: dict[str, ET.Element] = {}
|
||||||
|
|
||||||
for file in input_file_list:
|
for file in input_file_list:
|
||||||
tree = ET.parse(file)
|
tree = ET.parse(file)
|
||||||
@@ -389,7 +389,7 @@ class_names.sort()
|
|||||||
if len(input_class_list) < 1:
|
if len(input_class_list) < 1:
|
||||||
input_class_list = ["*"]
|
input_class_list = ["*"]
|
||||||
|
|
||||||
filtered_classes_set: Set[str] = set()
|
filtered_classes_set: set[str] = set()
|
||||||
for pattern in input_class_list:
|
for pattern in input_class_list:
|
||||||
filtered_classes_set |= set(fnmatch.filter(class_names, pattern))
|
filtered_classes_set |= set(fnmatch.filter(class_names, pattern))
|
||||||
filtered_classes = list(filtered_classes_set)
|
filtered_classes = list(filtered_classes_set)
|
||||||
@@ -419,7 +419,7 @@ for cn in filtered_classes:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
out = status.make_output()
|
out = status.make_output()
|
||||||
row: List[str] = []
|
row: list[str] = []
|
||||||
for column in table_columns:
|
for column in table_columns:
|
||||||
if column in out:
|
if column in out:
|
||||||
row.append(out[column])
|
row.append(out[column])
|
||||||
@@ -456,7 +456,7 @@ if flags["a"]:
|
|||||||
# without having to scroll back to the top.
|
# without having to scroll back to the top.
|
||||||
table.append(table_column_names)
|
table.append(table_column_names)
|
||||||
|
|
||||||
table_column_sizes: List[int] = []
|
table_column_sizes: list[int] = []
|
||||||
for row in table:
|
for row in table:
|
||||||
for cell_i, cell in enumerate(row):
|
for cell_i, cell in enumerate(row):
|
||||||
if cell_i >= len(table_column_sizes):
|
if cell_i >= len(table_column_sizes):
|
||||||
|
|||||||
113
doc/tools/make_rst.py
Executable file → Normal file
113
doc/tools/make_rst.py
Executable file → Normal file
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# This script makes RST files from the XML class reference for use with the online docs.
|
# This script makes RST files from the XML class reference for use with the online docs.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
@@ -8,7 +9,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Any, Dict, List, Optional, TextIO, Tuple, Union
|
from typing import Any, TextIO
|
||||||
|
|
||||||
sys.path.insert(0, root_directory := os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
|
sys.path.insert(0, root_directory := os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
|
||||||
|
|
||||||
@@ -90,10 +91,10 @@ BASE_STRINGS = [
|
|||||||
# See also `make_rst_class()` and `editor/doc/editor_help.cpp`.
|
# See also `make_rst_class()` and `editor/doc/editor_help.cpp`.
|
||||||
"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details.",
|
"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details.",
|
||||||
]
|
]
|
||||||
strings_l10n: Dict[str, str] = {}
|
strings_l10n: dict[str, str] = {}
|
||||||
writing_translation = False
|
writing_translation = False
|
||||||
|
|
||||||
CLASS_GROUPS: Dict[str, str] = {
|
CLASS_GROUPS: dict[str, str] = {
|
||||||
"global": "Globals",
|
"global": "Globals",
|
||||||
"node": "Nodes",
|
"node": "Nodes",
|
||||||
"resource": "Resources",
|
"resource": "Resources",
|
||||||
@@ -101,21 +102,21 @@ CLASS_GROUPS: Dict[str, str] = {
|
|||||||
"editor": "Editor-only",
|
"editor": "Editor-only",
|
||||||
"variant": "Variant types",
|
"variant": "Variant types",
|
||||||
}
|
}
|
||||||
CLASS_GROUPS_BASE: Dict[str, str] = {
|
CLASS_GROUPS_BASE: dict[str, str] = {
|
||||||
"node": "Node",
|
"node": "Node",
|
||||||
"resource": "Resource",
|
"resource": "Resource",
|
||||||
"object": "Object",
|
"object": "Object",
|
||||||
"variant": "Variant",
|
"variant": "Variant",
|
||||||
}
|
}
|
||||||
# Sync with editor\register_editor_types.cpp
|
# Sync with editor\register_editor_types.cpp
|
||||||
EDITOR_CLASSES: List[str] = [
|
EDITOR_CLASSES: list[str] = [
|
||||||
"FileSystemDock",
|
"FileSystemDock",
|
||||||
"ScriptCreateDialog",
|
"ScriptCreateDialog",
|
||||||
"ScriptEditor",
|
"ScriptEditor",
|
||||||
"ScriptEditorBase",
|
"ScriptEditorBase",
|
||||||
]
|
]
|
||||||
# Sync with the types mentioned in https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_differences.html
|
# Sync with the types mentioned in https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_differences.html
|
||||||
CLASSES_WITH_CSHARP_DIFFERENCES: List[str] = [
|
CLASSES_WITH_CSHARP_DIFFERENCES: list[str] = [
|
||||||
"@GlobalScope",
|
"@GlobalScope",
|
||||||
"String",
|
"String",
|
||||||
"StringName",
|
"StringName",
|
||||||
@@ -147,7 +148,7 @@ CLASSES_WITH_CSHARP_DIFFERENCES: List[str] = [
|
|||||||
"Variant",
|
"Variant",
|
||||||
]
|
]
|
||||||
|
|
||||||
PACKED_ARRAY_TYPES: List[str] = [
|
PACKED_ARRAY_TYPES: list[str] = [
|
||||||
"PackedByteArray",
|
"PackedByteArray",
|
||||||
"PackedColorArray",
|
"PackedColorArray",
|
||||||
"PackedFloat32Array",
|
"PackedFloat32Array",
|
||||||
@@ -425,7 +426,7 @@ class State:
|
|||||||
|
|
||||||
self.current_class = ""
|
self.current_class = ""
|
||||||
|
|
||||||
def parse_params(self, root: ET.Element, context: str) -> List["ParameterDef"]:
|
def parse_params(self, root: ET.Element, context: str) -> list[ParameterDef]:
|
||||||
param_elements = root.findall("param")
|
param_elements = root.findall("param")
|
||||||
params: Any = [None] * len(param_elements)
|
params: Any = [None] * len(param_elements)
|
||||||
|
|
||||||
@@ -443,7 +444,7 @@ class State:
|
|||||||
|
|
||||||
params[index] = ParameterDef(param_name, type_name, default)
|
params[index] = ParameterDef(param_name, type_name, default)
|
||||||
|
|
||||||
cast: List[ParameterDef] = params
|
cast: list[ParameterDef] = params
|
||||||
|
|
||||||
return cast
|
return cast
|
||||||
|
|
||||||
@@ -461,7 +462,7 @@ class TagState:
|
|||||||
|
|
||||||
|
|
||||||
class TypeName:
|
class TypeName:
|
||||||
def __init__(self, type_name: str, enum: Optional[str] = None, is_bitfield: bool = False) -> None:
|
def __init__(self, type_name: str, enum: str | None = None, is_bitfield: bool = False) -> None:
|
||||||
self.type_name = type_name
|
self.type_name = type_name
|
||||||
self.enum = enum
|
self.enum = enum
|
||||||
self.is_bitfield = is_bitfield
|
self.is_bitfield = is_bitfield
|
||||||
@@ -475,7 +476,7 @@ class TypeName:
|
|||||||
return make_type(self.type_name, state)
|
return make_type(self.type_name, state)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_element(cls, element: ET.Element) -> "TypeName":
|
def from_element(cls, element: ET.Element) -> TypeName:
|
||||||
return cls(element.attrib["type"], element.get("enum"), element.get("is_bitfield") == "true")
|
return cls(element.attrib["type"], element.get("enum"), element.get("is_bitfield") == "true")
|
||||||
|
|
||||||
|
|
||||||
@@ -487,8 +488,8 @@ class DefinitionBase:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.definition_name = definition_name
|
self.definition_name = definition_name
|
||||||
self.name = name
|
self.name = name
|
||||||
self.deprecated: Optional[str] = None
|
self.deprecated: str | None = None
|
||||||
self.experimental: Optional[str] = None
|
self.experimental: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PropertyDef(DefinitionBase):
|
class PropertyDef(DefinitionBase):
|
||||||
@@ -496,11 +497,11 @@ class PropertyDef(DefinitionBase):
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
type_name: TypeName,
|
type_name: TypeName,
|
||||||
setter: Optional[str],
|
setter: str | None,
|
||||||
getter: Optional[str],
|
getter: str | None,
|
||||||
text: Optional[str],
|
text: str | None,
|
||||||
default_value: Optional[str],
|
default_value: str | None,
|
||||||
overrides: Optional[str],
|
overrides: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__("property", name)
|
super().__init__("property", name)
|
||||||
|
|
||||||
@@ -513,7 +514,7 @@ class PropertyDef(DefinitionBase):
|
|||||||
|
|
||||||
|
|
||||||
class ParameterDef(DefinitionBase):
|
class ParameterDef(DefinitionBase):
|
||||||
def __init__(self, name: str, type_name: TypeName, default_value: Optional[str]) -> None:
|
def __init__(self, name: str, type_name: TypeName, default_value: str | None) -> None:
|
||||||
super().__init__("parameter", name)
|
super().__init__("parameter", name)
|
||||||
|
|
||||||
self.type_name = type_name
|
self.type_name = type_name
|
||||||
@@ -521,7 +522,7 @@ class ParameterDef(DefinitionBase):
|
|||||||
|
|
||||||
|
|
||||||
class SignalDef(DefinitionBase):
|
class SignalDef(DefinitionBase):
|
||||||
def __init__(self, name: str, parameters: List[ParameterDef], description: Optional[str]) -> None:
|
def __init__(self, name: str, parameters: list[ParameterDef], description: str | None) -> None:
|
||||||
super().__init__("signal", name)
|
super().__init__("signal", name)
|
||||||
|
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
@@ -532,9 +533,9 @@ class AnnotationDef(DefinitionBase):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
parameters: List[ParameterDef],
|
parameters: list[ParameterDef],
|
||||||
description: Optional[str],
|
description: str | None,
|
||||||
qualifiers: Optional[str],
|
qualifiers: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__("annotation", name)
|
super().__init__("annotation", name)
|
||||||
|
|
||||||
@@ -548,9 +549,9 @@ class MethodDef(DefinitionBase):
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
return_type: TypeName,
|
return_type: TypeName,
|
||||||
parameters: List[ParameterDef],
|
parameters: list[ParameterDef],
|
||||||
description: Optional[str],
|
description: str | None,
|
||||||
qualifiers: Optional[str],
|
qualifiers: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__("method", name)
|
super().__init__("method", name)
|
||||||
|
|
||||||
@@ -561,7 +562,7 @@ class MethodDef(DefinitionBase):
|
|||||||
|
|
||||||
|
|
||||||
class ConstantDef(DefinitionBase):
|
class ConstantDef(DefinitionBase):
|
||||||
def __init__(self, name: str, value: str, text: Optional[str], bitfield: bool) -> None:
|
def __init__(self, name: str, value: str, text: str | None, bitfield: bool) -> None:
|
||||||
super().__init__("constant", name)
|
super().__init__("constant", name)
|
||||||
|
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -580,7 +581,7 @@ class EnumDef(DefinitionBase):
|
|||||||
|
|
||||||
class ThemeItemDef(DefinitionBase):
|
class ThemeItemDef(DefinitionBase):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, type_name: TypeName, data_name: str, text: Optional[str], default_value: Optional[str]
|
self, name: str, type_name: TypeName, data_name: str, text: str | None, default_value: str | None
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__("theme property", name)
|
super().__init__("theme property", name)
|
||||||
|
|
||||||
@@ -600,17 +601,17 @@ class ClassDef(DefinitionBase):
|
|||||||
self.constants: OrderedDict[str, ConstantDef] = OrderedDict()
|
self.constants: OrderedDict[str, ConstantDef] = OrderedDict()
|
||||||
self.enums: OrderedDict[str, EnumDef] = OrderedDict()
|
self.enums: OrderedDict[str, EnumDef] = OrderedDict()
|
||||||
self.properties: OrderedDict[str, PropertyDef] = OrderedDict()
|
self.properties: OrderedDict[str, PropertyDef] = OrderedDict()
|
||||||
self.constructors: OrderedDict[str, List[MethodDef]] = OrderedDict()
|
self.constructors: OrderedDict[str, list[MethodDef]] = OrderedDict()
|
||||||
self.methods: OrderedDict[str, List[MethodDef]] = OrderedDict()
|
self.methods: OrderedDict[str, list[MethodDef]] = OrderedDict()
|
||||||
self.operators: OrderedDict[str, List[MethodDef]] = OrderedDict()
|
self.operators: OrderedDict[str, list[MethodDef]] = OrderedDict()
|
||||||
self.signals: OrderedDict[str, SignalDef] = OrderedDict()
|
self.signals: OrderedDict[str, SignalDef] = OrderedDict()
|
||||||
self.annotations: OrderedDict[str, List[AnnotationDef]] = OrderedDict()
|
self.annotations: OrderedDict[str, list[AnnotationDef]] = OrderedDict()
|
||||||
self.theme_items: OrderedDict[str, ThemeItemDef] = OrderedDict()
|
self.theme_items: OrderedDict[str, ThemeItemDef] = OrderedDict()
|
||||||
self.inherits: Optional[str] = None
|
self.inherits: str | None = None
|
||||||
self.brief_description: Optional[str] = None
|
self.brief_description: str | None = None
|
||||||
self.description: Optional[str] = None
|
self.description: str | None = None
|
||||||
self.tutorials: List[Tuple[str, str]] = []
|
self.tutorials: list[tuple[str, str]] = []
|
||||||
self.keywords: Optional[str] = None
|
self.keywords: str | None = None
|
||||||
|
|
||||||
# Used to match the class with XML source for output filtering purposes.
|
# Used to match the class with XML source for output filtering purposes.
|
||||||
self.filepath: str = ""
|
self.filepath: str = ""
|
||||||
@@ -656,7 +657,7 @@ class ClassDef(DefinitionBase):
|
|||||||
# which don't necessarily need C# examples.
|
# which don't necessarily need C# examples.
|
||||||
class ScriptLanguageParityCheck:
|
class ScriptLanguageParityCheck:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.hit_map: OrderedDict[str, List[Tuple[DefinitionBase, str]]] = OrderedDict()
|
self.hit_map: OrderedDict[str, list[tuple[DefinitionBase, str]]] = OrderedDict()
|
||||||
self.hit_count = 0
|
self.hit_count = 0
|
||||||
|
|
||||||
def add_hit(self, class_name: str, context: DefinitionBase, error: str, state: State) -> None:
|
def add_hit(self, class_name: str, context: DefinitionBase, error: str, state: State) -> None:
|
||||||
@@ -727,7 +728,7 @@ def main() -> None:
|
|||||||
|
|
||||||
print("Checking for errors in the XML class reference...")
|
print("Checking for errors in the XML class reference...")
|
||||||
|
|
||||||
file_list: List[str] = []
|
file_list: list[str] = []
|
||||||
|
|
||||||
for path in args.path:
|
for path in args.path:
|
||||||
# Cut off trailing slashes so os.path.basename doesn't choke.
|
# Cut off trailing slashes so os.path.basename doesn't choke.
|
||||||
@@ -751,7 +752,7 @@ def main() -> None:
|
|||||||
|
|
||||||
file_list.append(path)
|
file_list.append(path)
|
||||||
|
|
||||||
classes: Dict[str, Tuple[ET.Element, str]] = {}
|
classes: dict[str, tuple[ET.Element, str]] = {}
|
||||||
state = State()
|
state = State()
|
||||||
|
|
||||||
for cur_file in file_list:
|
for cur_file in file_list:
|
||||||
@@ -784,7 +785,7 @@ def main() -> None:
|
|||||||
|
|
||||||
print("Generating the RST class reference...")
|
print("Generating the RST class reference...")
|
||||||
|
|
||||||
grouped_classes: Dict[str, List[str]] = {}
|
grouped_classes: dict[str, list[str]] = {}
|
||||||
|
|
||||||
for class_name, class_def in state.classes.items():
|
for class_name, class_def in state.classes.items():
|
||||||
if args.filter and not pattern.search(class_def.filepath):
|
if args.filter and not pattern.search(class_def.filepath):
|
||||||
@@ -947,7 +948,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
|
|||||||
f.write("\n\n")
|
f.write("\n\n")
|
||||||
|
|
||||||
# Descendants
|
# Descendants
|
||||||
inherited: List[str] = []
|
inherited: list[str] = []
|
||||||
for c in state.classes.values():
|
for c in state.classes.values():
|
||||||
if c.inherits and c.inherits.strip() == class_name:
|
if c.inherits and c.inherits.strip() == class_name:
|
||||||
inherited.append(c.name)
|
inherited.append(c.name)
|
||||||
@@ -1008,7 +1009,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
|
|||||||
### REFERENCE TABLES ###
|
### REFERENCE TABLES ###
|
||||||
|
|
||||||
# Reused container for reference tables.
|
# Reused container for reference tables.
|
||||||
ml: List[Tuple[Optional[str], ...]] = []
|
ml: list[tuple[str | None, ...]] = []
|
||||||
|
|
||||||
# Properties reference table
|
# Properties reference table
|
||||||
if len(class_def.properties) > 0:
|
if len(class_def.properties) > 0:
|
||||||
@@ -1544,8 +1545,8 @@ def make_enum(t: str, is_bitfield: bool, state: State) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def make_method_signature(
|
def make_method_signature(
|
||||||
class_def: ClassDef, definition: Union[AnnotationDef, MethodDef, SignalDef], ref_type: str, state: State
|
class_def: ClassDef, definition: AnnotationDef | MethodDef | SignalDef, ref_type: str, state: State
|
||||||
) -> Tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
ret_type = ""
|
ret_type = ""
|
||||||
|
|
||||||
if isinstance(definition, MethodDef):
|
if isinstance(definition, MethodDef):
|
||||||
@@ -1611,7 +1612,7 @@ def make_setter_signature(class_def: ClassDef, property_def: PropertyDef, state:
|
|||||||
setter = class_def.methods[property_def.setter][0]
|
setter = class_def.methods[property_def.setter][0]
|
||||||
# Otherwise we fake it with the information we have available.
|
# Otherwise we fake it with the information we have available.
|
||||||
else:
|
else:
|
||||||
setter_params: List[ParameterDef] = []
|
setter_params: list[ParameterDef] = []
|
||||||
setter_params.append(ParameterDef("value", property_def.type_name, None))
|
setter_params.append(ParameterDef("value", property_def.type_name, None))
|
||||||
setter = MethodDef(property_def.setter, TypeName("void"), setter_params, None, None)
|
setter = MethodDef(property_def.setter, TypeName("void"), setter_params, None, None)
|
||||||
|
|
||||||
@@ -1628,7 +1629,7 @@ def make_getter_signature(class_def: ClassDef, property_def: PropertyDef, state:
|
|||||||
getter = class_def.methods[property_def.getter][0]
|
getter = class_def.methods[property_def.getter][0]
|
||||||
# Otherwise we fake it with the information we have available.
|
# Otherwise we fake it with the information we have available.
|
||||||
else:
|
else:
|
||||||
getter_params: List[ParameterDef] = []
|
getter_params: list[ParameterDef] = []
|
||||||
getter = MethodDef(property_def.getter, property_def.type_name, getter_params, None, None)
|
getter = MethodDef(property_def.getter, property_def.type_name, getter_params, None, None)
|
||||||
|
|
||||||
ret_type, signature = make_method_signature(class_def, getter, "", state)
|
ret_type, signature = make_method_signature(class_def, getter, "", state)
|
||||||
@@ -1727,7 +1728,7 @@ def make_link(url: str, title: str) -> str:
|
|||||||
return f"`{url} <{url}>`__"
|
return f"`{url} <{url}>`__"
|
||||||
|
|
||||||
|
|
||||||
def make_rst_index(grouped_classes: Dict[str, List[str]], dry_run: bool, output_dir: str) -> None:
|
def make_rst_index(grouped_classes: dict[str, list[str]], dry_run: bool, output_dir: str) -> None:
|
||||||
with open(
|
with open(
|
||||||
os.devnull if dry_run else os.path.join(output_dir, "index.rst"), "w", encoding="utf-8", newline="\n"
|
os.devnull if dry_run else os.path.join(output_dir, "index.rst"), "w", encoding="utf-8", newline="\n"
|
||||||
) as f:
|
) as f:
|
||||||
@@ -1792,7 +1793,7 @@ RESERVED_CROSSLINK_TAGS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def is_in_tagset(tag_text: str, tagset: List[str]) -> bool:
|
def is_in_tagset(tag_text: str, tagset: list[str]) -> bool:
|
||||||
for tag in tagset:
|
for tag in tagset:
|
||||||
# Complete match.
|
# Complete match.
|
||||||
if tag_text == tag:
|
if tag_text == tag:
|
||||||
@@ -1834,7 +1835,7 @@ def get_tag_and_args(tag_text: str) -> TagState:
|
|||||||
return TagState(tag_text, tag_name, arguments, closing)
|
return TagState(tag_text, tag_name, arguments, closing)
|
||||||
|
|
||||||
|
|
||||||
def parse_link_target(link_target: str, state: State, context_name: str) -> List[str]:
|
def parse_link_target(link_target: str, state: State, context_name: str) -> list[str]:
|
||||||
if link_target.find(".") != -1:
|
if link_target.find(".") != -1:
|
||||||
return link_target.split(".")
|
return link_target.split(".")
|
||||||
else:
|
else:
|
||||||
@@ -2082,7 +2083,7 @@ def format_text_block(
|
|||||||
|
|
||||||
valid_param_context = isinstance(context, (MethodDef, SignalDef, AnnotationDef))
|
valid_param_context = isinstance(context, (MethodDef, SignalDef, AnnotationDef))
|
||||||
if valid_param_context:
|
if valid_param_context:
|
||||||
context_params: List[ParameterDef] = context.parameters # type: ignore
|
context_params: list[ParameterDef] = context.parameters # type: ignore
|
||||||
for param_def in context_params:
|
for param_def in context_params:
|
||||||
if param_def.name == inside_code_text:
|
if param_def.name == inside_code_text:
|
||||||
print_warning(
|
print_warning(
|
||||||
@@ -2242,7 +2243,7 @@ def format_text_block(
|
|||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
context_params: List[ParameterDef] = context.parameters # type: ignore
|
context_params: list[ParameterDef] = context.parameters # type: ignore
|
||||||
found = False
|
found = False
|
||||||
for param_def in context_params:
|
for param_def in context_params:
|
||||||
if param_def.name == link_target:
|
if param_def.name == link_target:
|
||||||
@@ -2407,7 +2408,7 @@ def format_text_block(
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def preformat_text_block(text: str, state: State) -> Optional[str]:
|
def preformat_text_block(text: str, state: State) -> str | None:
|
||||||
result = ""
|
result = ""
|
||||||
codeblock_tag = ""
|
codeblock_tag = ""
|
||||||
indent_level = 0
|
indent_level = 0
|
||||||
@@ -2457,7 +2458,7 @@ def preformat_text_block(text: str, state: State) -> Optional[str]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def format_context_name(context: Union[DefinitionBase, None]) -> str:
|
def format_context_name(context: DefinitionBase | None) -> str:
|
||||||
context_name: str = "unknown context"
|
context_name: str = "unknown context"
|
||||||
if context is not None:
|
if context is not None:
|
||||||
context_name = f'{context.definition_name} "{context.name}" description'
|
context_name = f'{context.definition_name} "{context.name}" description'
|
||||||
@@ -2499,7 +2500,7 @@ def escape_rst(text: str, until_pos: int = -1) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_columns: bool = False) -> None:
|
def format_table(f: TextIO, data: list[tuple[str | None, ...]], remove_empty_columns: bool = False) -> None:
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2544,7 +2545,7 @@ def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_
|
|||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
def sanitize_class_name(dirty_name: str, is_file_name=False) -> str:
|
def sanitize_class_name(dirty_name: str, is_file_name: bool = False) -> str:
|
||||||
if is_file_name:
|
if is_file_name:
|
||||||
return dirty_name.lower().replace('"', "").replace("/", "--")
|
return dirty_name.lower().replace('"', "").replace("/", "--")
|
||||||
else:
|
else:
|
||||||
|
|||||||
14
methods.py
14
methods.py
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
import contextlib
|
import contextlib
|
||||||
import glob
|
import glob
|
||||||
@@ -9,9 +11,9 @@ import sys
|
|||||||
import textwrap
|
import textwrap
|
||||||
import zlib
|
import zlib
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from io import StringIO, TextIOBase
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator, List, Optional, Union, cast
|
from typing import Generator, TextIO, cast
|
||||||
|
|
||||||
from misc.utility.color import print_error, print_info, print_warning
|
from misc.utility.color import print_error, print_info, print_warning
|
||||||
from platform_methods import detect_arch
|
from platform_methods import detect_arch
|
||||||
@@ -1553,8 +1555,8 @@ def generate_copyright_header(filename: str) -> str:
|
|||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def generated_wrapper(
|
def generated_wrapper(
|
||||||
path: str,
|
path: str,
|
||||||
guard: Optional[bool] = None,
|
guard: bool | None = None,
|
||||||
) -> Generator[TextIOBase, None, None]:
|
) -> Generator[TextIO, None, None]:
|
||||||
"""
|
"""
|
||||||
Wrapper class to automatically handle copyright headers and header guards
|
Wrapper class to automatically handle copyright headers and header guards
|
||||||
for generated scripts. Meant to be invoked via `with` statement similar to
|
for generated scripts. Meant to be invoked via `with` statement similar to
|
||||||
@@ -1626,13 +1628,13 @@ def to_escaped_cstring(value: str) -> str:
|
|||||||
return value.translate(C_ESCAPE_TABLE)
|
return value.translate(C_ESCAPE_TABLE)
|
||||||
|
|
||||||
|
|
||||||
def to_raw_cstring(value: Union[str, List[str]]) -> str:
|
def to_raw_cstring(value: str | list[str]) -> str:
|
||||||
MAX_LITERAL = 16 * 1024
|
MAX_LITERAL = 16 * 1024
|
||||||
|
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
value = "\n".join(value) + "\n"
|
value = "\n".join(value) + "\n"
|
||||||
|
|
||||||
split: List[bytes] = []
|
split: list[bytes] = []
|
||||||
offset = 0
|
offset = 0
|
||||||
encoded = value.encode()
|
encoded = value.encode()
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
# the Unicode Character Database to the `char_range.inc` file.
|
# the Unicode Character Database to the `char_range.inc` file.
|
||||||
# NOTE: This script is deliberately not integrated into the build system;
|
# NOTE: This script is deliberately not integrated into the build system;
|
||||||
# you should run it manually whenever you want to update the data.
|
# you should run it manually whenever you want to update the data.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Final, List, Tuple
|
from typing import Final
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -18,20 +19,20 @@ from methods import generate_copyright_header
|
|||||||
URL: Final[str] = "https://www.unicode.org/Public/16.0.0/ucd/DerivedCoreProperties.txt"
|
URL: Final[str] = "https://www.unicode.org/Public/16.0.0/ucd/DerivedCoreProperties.txt"
|
||||||
|
|
||||||
|
|
||||||
xid_start: List[Tuple[int, int]] = []
|
xid_start: list[tuple[int, int]] = []
|
||||||
xid_continue: List[Tuple[int, int]] = []
|
xid_continue: list[tuple[int, int]] = []
|
||||||
uppercase_letter: List[Tuple[int, int]] = []
|
uppercase_letter: list[tuple[int, int]] = []
|
||||||
lowercase_letter: List[Tuple[int, int]] = []
|
lowercase_letter: list[tuple[int, int]] = []
|
||||||
unicode_letter: List[Tuple[int, int]] = []
|
unicode_letter: list[tuple[int, int]] = []
|
||||||
|
|
||||||
|
|
||||||
def merge_ranges(ranges: List[Tuple[int, int]]) -> None:
|
def merge_ranges(ranges: list[tuple[int, int]]) -> None:
|
||||||
if len(ranges) < 2:
|
if len(ranges) < 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
last_start: int = ranges[0][0]
|
last_start: int = ranges[0][0]
|
||||||
last_end: int = ranges[0][1]
|
last_end: int = ranges[0][1]
|
||||||
original_ranges: List[Tuple[int, int]] = ranges[1:]
|
original_ranges: list[tuple[int, int]] = ranges[1:]
|
||||||
|
|
||||||
ranges.clear()
|
ranges.clear()
|
||||||
|
|
||||||
@@ -47,13 +48,13 @@ def merge_ranges(ranges: List[Tuple[int, int]]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def parse_unicode_data() -> None:
|
def parse_unicode_data() -> None:
|
||||||
lines: List[str] = [line.decode("utf-8") for line in urlopen(URL)]
|
lines: list[str] = [line.decode("utf-8") for line in urlopen(URL)]
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.startswith("#") or not line.strip():
|
if line.startswith("#") or not line.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
split_line: List[str] = line.split(";")
|
split_line: list[str] = line.split(";")
|
||||||
|
|
||||||
char_range: str = split_line[0].strip()
|
char_range: str = split_line[0].strip()
|
||||||
char_property: str = split_line[1].strip().split("#")[0].strip()
|
char_property: str = split_line[1].strip().split("#")[0].strip()
|
||||||
@@ -63,7 +64,7 @@ def parse_unicode_data() -> None:
|
|||||||
if ".." in char_range:
|
if ".." in char_range:
|
||||||
range_start, range_end = char_range.split("..")
|
range_start, range_end = char_range.split("..")
|
||||||
|
|
||||||
range_tuple: Tuple[int, int] = (int(range_start, 16), int(range_end, 16))
|
range_tuple: tuple[int, int] = (int(range_start, 16), int(range_end, 16))
|
||||||
|
|
||||||
if char_property == "XID_Start":
|
if char_property == "XID_Start":
|
||||||
xid_start.append(range_tuple)
|
xid_start.append(range_tuple)
|
||||||
@@ -87,7 +88,7 @@ def parse_unicode_data() -> None:
|
|||||||
merge_ranges(unicode_letter)
|
merge_ranges(unicode_letter)
|
||||||
|
|
||||||
|
|
||||||
def make_array(array_name: str, range_list: List[Tuple[int, int]]) -> str:
|
def make_array(array_name: str, range_list: list[tuple[int, int]]) -> str:
|
||||||
result: str = f"\n\nconstexpr inline CharRange {array_name}[] = {{\n"
|
result: str = f"\n\nconstexpr inline CharRange {array_name}[] = {{\n"
|
||||||
|
|
||||||
for start, end in range_list:
|
for start, end in range_list:
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
# the Unicode Character Database to the `ucaps.h` file.
|
# the Unicode Character Database to the `ucaps.h` file.
|
||||||
# NOTE: This script is deliberately not integrated into the build system;
|
# NOTE: This script is deliberately not integrated into the build system;
|
||||||
# you should run it manually whenever you want to update the data.
|
# you should run it manually whenever you want to update the data.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Final, List, Tuple
|
from typing import Final
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -18,15 +19,15 @@ from methods import generate_copyright_header
|
|||||||
URL: Final[str] = "https://www.unicode.org/Public/16.0.0/ucd/UnicodeData.txt"
|
URL: Final[str] = "https://www.unicode.org/Public/16.0.0/ucd/UnicodeData.txt"
|
||||||
|
|
||||||
|
|
||||||
lower_to_upper: List[Tuple[str, str]] = []
|
lower_to_upper: list[tuple[str, str]] = []
|
||||||
upper_to_lower: List[Tuple[str, str]] = []
|
upper_to_lower: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
|
||||||
def parse_unicode_data() -> None:
|
def parse_unicode_data() -> None:
|
||||||
lines: List[str] = [line.decode("utf-8") for line in urlopen(URL)]
|
lines: list[str] = [line.decode("utf-8") for line in urlopen(URL)]
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
split_line: List[str] = line.split(";")
|
split_line: list[str] = line.split(";")
|
||||||
|
|
||||||
code_value: str = split_line[0].strip()
|
code_value: str = split_line[0].strip()
|
||||||
uppercase_mapping: str = split_line[12].strip()
|
uppercase_mapping: str = split_line[12].strip()
|
||||||
@@ -38,7 +39,7 @@ def parse_unicode_data() -> None:
|
|||||||
upper_to_lower.append((f"0x{code_value}", f"0x{lowercase_mapping}"))
|
upper_to_lower.append((f"0x{code_value}", f"0x{lowercase_mapping}"))
|
||||||
|
|
||||||
|
|
||||||
def make_cap_table(table_name: str, len_name: str, table: List[Tuple[str, str]]) -> str:
|
def make_cap_table(table_name: str, len_name: str, table: list[tuple[str, str]]) -> str:
|
||||||
result: str = f"static const int {table_name}[{len_name}][2] = {{\n"
|
result: str = f"static const int {table_name}[{len_name}][2] = {{\n"
|
||||||
|
|
||||||
for first, second in table:
|
for first, second in table:
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
# the Unicode Character Database to the `char_range.inc` file.
|
# the Unicode Character Database to the `char_range.inc` file.
|
||||||
# NOTE: This script is deliberately not integrated into the build system;
|
# NOTE: This script is deliberately not integrated into the build system;
|
||||||
# you should run it manually whenever you want to update the data.
|
# you should run it manually whenever you want to update the data.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Final, List, Set, Tuple
|
from typing import Final
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -18,9 +19,9 @@ from methods import generate_copyright_header
|
|||||||
URL: Final[str] = "https://www.unicode.org/Public/16.0.0/ucd/Blocks.txt"
|
URL: Final[str] = "https://www.unicode.org/Public/16.0.0/ucd/Blocks.txt"
|
||||||
|
|
||||||
|
|
||||||
ranges: List[Tuple[str, str, str]] = []
|
ranges: list[tuple[str, str, str]] = []
|
||||||
|
|
||||||
exclude_blocks: Set[str] = {
|
exclude_blocks: set[str] = {
|
||||||
"High Surrogates",
|
"High Surrogates",
|
||||||
"High Private Use Surrogates",
|
"High Private Use Surrogates",
|
||||||
"Low Surrogates",
|
"Low Surrogates",
|
||||||
@@ -33,13 +34,13 @@ exclude_blocks: Set[str] = {
|
|||||||
|
|
||||||
|
|
||||||
def parse_unicode_data() -> None:
|
def parse_unicode_data() -> None:
|
||||||
lines: List[str] = [line.decode("utf-8") for line in urlopen(URL)]
|
lines: list[str] = [line.decode("utf-8") for line in urlopen(URL)]
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.startswith("#") or not line.strip():
|
if line.startswith("#") or not line.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
split_line: List[str] = line.split(";")
|
split_line: list[str] = line.split(";")
|
||||||
|
|
||||||
char_range: str = split_line[0].strip()
|
char_range: str = split_line[0].strip()
|
||||||
block: str = split_line[1].strip()
|
block: str = split_line[1].strip()
|
||||||
@@ -52,7 +53,7 @@ def parse_unicode_data() -> None:
|
|||||||
ranges.append((f"0x{range_start}", f"0x{range_end}", block))
|
ranges.append((f"0x{range_start}", f"0x{range_end}", block))
|
||||||
|
|
||||||
|
|
||||||
def make_array(array_name: str, ranges: List[Tuple[str, str, str]]) -> str:
|
def make_array(array_name: str, ranges: list[tuple[str, str, str]]) -> str:
|
||||||
result: str = f"static UniRange {array_name}[] = {{\n"
|
result: str = f"static UniRange {array_name}[] = {{\n"
|
||||||
|
|
||||||
for start, end, block in ranges:
|
for start, end, block in ranges:
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def find_dotnet_cli():
|
def find_dotnet_cli():
|
||||||
@@ -151,7 +151,7 @@ def find_any_msbuild_tool(mono_prefix):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def run_msbuild(tools: ToolsLocation, sln: str, chdir_to: str, msbuild_args: Optional[List[str]] = None):
|
def run_msbuild(tools: ToolsLocation, sln: str, chdir_to: str, msbuild_args: list[str] | None = None):
|
||||||
using_msbuild_mono = False
|
using_msbuild_mono = False
|
||||||
|
|
||||||
# Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
|
# Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ target-version = "py38"
|
|||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"I", # isort
|
"I", # isort
|
||||||
|
"UP006", # Use {to} instead of {from} for type annotation
|
||||||
|
"UP007", # Use `X | Y` for type annotations
|
||||||
|
"UP037", # Remove quotes from type annotation
|
||||||
|
"FA", # Future annotations
|
||||||
]
|
]
|
||||||
|
extend-safe-fixes = ["UP006", "UP007", "FA"]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"{SConstruct,SCsub}" = [
|
"{SConstruct,SCsub}" = [
|
||||||
|
|||||||
Reference in New Issue
Block a user