From c9669d043d7f2b9af8fb87074f715f582090a44d Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 11:11:55 +0200 Subject: [PATCH 01/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.diagrams=5Fcommon`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/diagrams_common.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 072a56a09..5184fafc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,6 @@ ignore_missing_imports = true module = [ 'sphinx_needs.api.need', 'sphinx_needs.data', - 'sphinx_needs.diagrams_common', 'sphinx_needs.directives.need', 'sphinx_needs.directives.needbar', 'sphinx_needs.directives.needextend', diff --git a/sphinx_needs/diagrams_common.py b/sphinx_needs/diagrams_common.py index 59598286e..ecef357a4 100644 --- a/sphinx_needs/diagrams_common.py +++ b/sphinx_needs/diagrams_common.py @@ -121,7 +121,7 @@ def get_filter_para(node_element: nodes.Element) -> nodes.paragraph: return para -def get_debug_container(puml_node: nodes.Node) -> nodes.container: +def get_debug_container(puml_node: nodes.Element) -> nodes.container: """Return container containing the raw plantuml code""" debug_container = nodes.container() if isinstance(puml_node, nodes.figure): From ca029c2a2bf1c524b505475ce243d3dc7045a428 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 11:23:38 +0200 Subject: [PATCH 02/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.warnings`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/warnings.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5184fafc0..f115b4977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,6 @@ module = [ 'sphinx_needs.services.github', 'sphinx_needs.services.manager', 'sphinx_needs.utils', - 'sphinx_needs.warnings', ] ignore_errors = true diff --git a/sphinx_needs/warnings.py b/sphinx_needs/warnings.py index 4eb23bcab..950f138a0 100644 --- a/sphinx_needs/warnings.py +++ b/sphinx_needs/warnings.py @@ -43,7 +43,7 @@ def process_warnings(app: Sphinx, exception: Optional[Exception]) -> None: if hasattr(env, "needs_warnings_executed") and env.needs_warnings_executed: return - env.needs_warnings_executed = True + env.needs_warnings_executed = True # type: ignore[attr-defined] needs = SphinxNeedsData(env).get_or_create_needs() From 99b88d58a068de7c1d0ecaa964a4a595f0aac053 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 13:11:14 +0200 Subject: [PATCH 03/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.environment`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/environment.py | 35 +++++++++++++++++------------------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f115b4977..30653f51b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,7 +111,6 @@ module = [ 'sphinx_needs.directives.needtable', 'sphinx_needs.directives.needuml', 'sphinx_needs.directives.utils', - 'sphinx_needs.environment', 'sphinx_needs.external_needs', 'sphinx_needs.filter_common', 'sphinx_needs.functions.common', diff --git a/sphinx_needs/environment.py b/sphinx_needs/environment.py index 0fb683b2a..09cb7c9a2 100644 --- a/sphinx_needs/environment.py +++ b/sphinx_needs/environment.py @@ -1,11 +1,11 @@ from pathlib import Path, PurePosixPath -from typing import Iterable +from typing import Iterable, List from jinja2 import Environment, PackageLoader, select_autoescape from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.util import status_iterator -from sphinx.util.console import brown +from sphinx.util.console import brown # type: ignore[attr-defined] from sphinx.util.osutil import copyfile from sphinx_needs.config import NeedsSphinxConfig @@ -28,18 +28,18 @@ def safe_add_file(filename: Path, app: Sphinx) -> None: """ builder = unwrap(app.builder) # Use PurePosixPath, so that the path can be used as "web"-path - filename = PurePosixPath(filename) - static_data_file = PurePosixPath("_static") / filename + pure_path = PurePosixPath(filename) + static_data_file = PurePosixPath("_static") / pure_path - if filename.suffix == ".js": + if pure_path.suffix == ".js": # Make sure the calculated (posix)-path is not already registered as "web"-path if hasattr(builder, "script_files") and str(static_data_file) not in builder.script_files: - app.add_js_file(str(filename)) - elif filename.suffix == ".css": + app.add_js_file(str(pure_path)) + elif pure_path.suffix == ".css": if hasattr(builder, "css_files") and str(static_data_file) not in builder.css_files: - app.add_css_file(str(filename)) + app.add_css_file(str(pure_path)) else: - raise NotImplementedError(f"File type {filename.suffix} not support by save_add_file") + raise NotImplementedError(f"File type {pure_path.suffix} not support by save_add_file") def safe_remove_file(filename: Path, app: Sphinx) -> None: @@ -54,10 +54,9 @@ def safe_remove_file(filename: Path, app: Sphinx) -> None: :param app: app object :return: None """ - static_data_file = Path("_static") / filename - static_data_file = PurePosixPath(static_data_file) + static_data_file = PurePosixPath(Path("_static") / filename) - def remove_file(file: Path, attribute: str) -> None: + def _remove_file(file: PurePosixPath, attribute: str) -> None: files = getattr(app.builder, attribute, []) if str(file) in files: files.remove(str(file)) @@ -69,7 +68,7 @@ def remove_file(file: Path, attribute: str) -> None: attribute = attributes.get(filename.suffix) if attribute: - remove_file(static_data_file, attribute) + _remove_file(static_data_file, attribute) # Base implementation from sphinxcontrib-images @@ -84,16 +83,16 @@ def install_styles_static_files(app: Sphinx, env: BuildEnvironment) -> None: css_root = Path(__file__).parent / "css" dest_dir = statics_dir / "sphinx-needs" - def find_css_files() -> Iterable[Path]: + def _find_css_files() -> Iterable[Path]: needs_css = NeedsSphinxConfig(app.config).css for theme in ["modern", "dark", "blank"]: if needs_css == f"{theme}.css": css_dir = css_root / theme return [f for f in css_dir.glob("**/*") if f.is_file()] - return [needs_css] + return [Path(needs_css)] files_to_copy = [Path("common.css")] - files_to_copy.extend(find_css_files()) + files_to_copy.extend(_find_css_files()) # Be sure no "old" css layout is already set for theme in ["common", "modern", "dark", "blank"]: @@ -105,7 +104,7 @@ def find_css_files() -> Iterable[Path]: "Copying static files for sphinx-needs custom style support...", brown, length=len(files_to_copy), - stringify_func=lambda x: Path(x).name, + stringify_func=lambda x: x.name, ): source_file_path = Path(source_file_path) @@ -129,7 +128,7 @@ def install_static_files( app: Sphinx, source_dir: Path, destination_dir: Path, - files_to_copy: Iterable[Path], + files_to_copy: List[Path], message: str, ) -> None: builder = unwrap(app.builder) From 1bbf9db36fce141f72d7486862e259fdb518f2f9 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 13:29:13 +0200 Subject: [PATCH 04/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.needs`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/config.py | 2 +- sphinx_needs/debug.py | 2 +- sphinx_needs/functions/__init__.py | 11 ++++++++++- sphinx_needs/functions/functions.py | 2 +- sphinx_needs/needs.py | 14 ++++++-------- sphinx_needs/utils.py | 4 ++-- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 30653f51b..8cbca889c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,6 @@ module = [ 'sphinx_needs.functions.common', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', - 'sphinx_needs.needs', 'sphinx_needs.needsfile', 'sphinx_needs.roles.need_incoming', 'sphinx_needs.roles.need_outgoing', diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index 6851fcc19..ba4d38e50 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -145,7 +145,7 @@ def __setattr__(self, name: str, value: Any) -> None: default=DEFAULT_DIAGRAM_TEMPLATE, metadata={"rebuild": "html", "types": (str,)}, ) - functions: list[Any] = field(default_factory=list, metadata={"rebuild": "html", "types": (list,)}) + functions: list[Callable[..., Any]] = field(default_factory=list, metadata={"rebuild": "html", "types": (list,)}) global_options: dict[str, Any] = field(default_factory=dict, metadata={"rebuild": "html", "types": (dict,)}) duration_option: str = field(default="duration", metadata={"rebuild": "html", "types": (str,)}) completion_option: str = field(default="completion", metadata={"rebuild": "html", "types": (str,)}) diff --git a/sphinx_needs/debug.py b/sphinx_needs/debug.py index 9c3a991dd..7243c6c57 100644 --- a/sphinx_needs/debug.py +++ b/sphinx_needs/debug.py @@ -18,7 +18,7 @@ TIME_MEASUREMENTS: Dict[str, Any] = {} # Stores the timing results EXECUTE_TIME_MEASUREMENTS = False # Will be used to de/activate measurements. Set during a Sphinx Event -START_TIME = 0 +START_TIME = 0.0 def measure_time( diff --git a/sphinx_needs/functions/__init__.py b/sphinx_needs/functions/__init__.py index d5f356d92..e3bb471bd 100644 --- a/sphinx_needs/functions/__init__.py +++ b/sphinx_needs/functions/__init__.py @@ -1,3 +1,5 @@ +from typing import Any, Callable, List + from sphinx_needs.functions.common import ( calc_sum, check_linked_values, @@ -15,4 +17,11 @@ resolve_variants_options, ) -needs_common_functions = [test, echo, copy, check_linked_values, calc_sum, links_from_content] +NEEDS_COMMON_FUNCTIONS: List[Callable[..., Any]] = [ + test, + echo, + copy, + check_linked_values, + calc_sum, + links_from_content, +] diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index b50ad1b8d..60f95071e 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -29,7 +29,7 @@ DynamicFunction = Callable[[Sphinx, Any, Any], Union[str, int, float, List[Union[str, int, float]]]] -def register_func(need_function: DynamicFunction, name: Optional[str] = None): +def register_func(need_function: DynamicFunction, name: Optional[str] = None) -> None: """ Registers a new sphinx-needs function for the given sphinx environment. :param env: Sphinx environment diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index 876822e50..03831baee 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -88,7 +88,7 @@ install_styles_static_files, ) from sphinx_needs.external_needs import load_external_needs -from sphinx_needs.functions import needs_common_functions, register_func +from sphinx_needs.functions import NEEDS_COMMON_FUNCTIONS, register_func from sphinx_needs.logging import get_logger from sphinx_needs.roles import NeedsXRefRole from sphinx_needs.roles.need_count import NeedCount, process_need_count @@ -273,7 +273,7 @@ def process_caller(app: Sphinx, doctree: nodes.document, fromdocname: str) -> No and fromdocname != f"{app.config.root_doc}" ): return - current_nodes = {} + current_nodes: Dict[Type[nodes.Element], List[nodes.Element]] = {} check_nodes = list(node_list.keys()) for node_need in doctree.findall(node_match(check_nodes)): for check_node in node_list: @@ -292,7 +292,7 @@ def process_caller(app: Sphinx, doctree: nodes.document, fromdocname: str) -> No return process_caller -def load_config(app: Sphinx, *_args) -> None: +def load_config(app: Sphinx, *_args: Any) -> None: """ Register extra options and directive based on config from conf.py """ @@ -388,7 +388,7 @@ def load_config(app: Sphinx, *_args) -> None: log.warning(f'{name} for "warnings" is already registered. [needs]', type="needs") -def visitor_dummy(*_args, **_kwargs) -> None: +def visitor_dummy(*_args: Any, **_kwargs: Any) -> None: """ Dummy class for visitor methods, which does nothing. """ @@ -420,14 +420,12 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docname: str) -> None: # Otherwise, the service may get registered later by an external sphinx-needs extension services.register(name, service["class"], **service["class_init"]) - needs_functions = needs_config.functions - # Register built-in functions - for need_common_func in needs_common_functions: + for need_common_func in NEEDS_COMMON_FUNCTIONS: register_func(need_common_func) # Register functions configured by user - for needs_func in needs_functions: + for needs_func in needs_config.functions: register_func(needs_func) # Own extra options diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index f648d4ae2..9f3194a52 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -5,7 +5,7 @@ import re from functools import reduce, wraps from re import Pattern -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union from urllib.parse import urlparse from docutils import nodes @@ -554,7 +554,7 @@ def unwrap(obj: Optional[T]) -> T: return obj -def node_match(node_types): +def node_match(node_types: Union[Type[nodes.Element], List[Type[nodes.Element]]]) -> Callable[[nodes.Node], bool]: """ Returns a condition function for doctuils.nodes.findall() From afbf32a1c73c4ed4c133fd744c44519d38d03f3f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 13:34:37 +0200 Subject: [PATCH 05/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.services.manager`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/services/base.py | 4 +++- sphinx_needs/services/manager.py | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8cbca889c..8cdeb8ef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,6 @@ module = [ 'sphinx_needs.roles.need_part', 'sphinx_needs.roles.need_ref', 'sphinx_needs.services.github', - 'sphinx_needs.services.manager', 'sphinx_needs.utils', ] ignore_errors = true diff --git a/sphinx_needs/services/base.py b/sphinx_needs/services/base.py index 804614388..36c60e82d 100644 --- a/sphinx_needs/services/base.py +++ b/sphinx_needs/services/base.py @@ -1,9 +1,11 @@ -from typing import Any +from typing import Any, ClassVar, List from sphinx_needs.logging import get_logger class BaseService: + options: ClassVar[List[str]] + def __init__(self, *args: Any, **kwargs: Any) -> None: self.log = get_logger(__name__) diff --git a/sphinx_needs/services/manager.py b/sphinx_needs/services/manager.py index 1d58fb63c..a0f3f8668 100644 --- a/sphinx_needs/services/manager.py +++ b/sphinx_needs/services/manager.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Any, Dict, Type from docutils.parsers.rst import directives from sphinx.application import Sphinx @@ -17,7 +17,7 @@ def __init__(self, app: Sphinx): self.log = get_logger(__name__) self.services: Dict[str, BaseService] = {} - def register(self, name: str, clazz, **kwargs) -> None: + def register(self, name: str, klass: Type[BaseService], **kwargs: Any) -> None: try: config = NeedsSphinxConfig(self.app.config).services[name] except KeyError: @@ -25,7 +25,7 @@ def register(self, name: str, clazz, **kwargs) -> None: config = {} # Register options from service class - for option in clazz.options: + for option in klass.options: if option not in NEEDS_CONFIG.extra_options: self.log.debug(f'Register option "{option}" for service "{name}"') NEEDS_CONFIG.extra_options[option] = directives.unchanged @@ -34,7 +34,7 @@ def register(self, name: str, clazz, **kwargs) -> None: NeedserviceDirective.option_spec[option] = directives.unchanged # Init service with custom config - self.services[name] = clazz(self.app, name, config, **kwargs) + self.services[name] = klass(self.app, name, config, **kwargs) def get(self, name: str) -> BaseService: if name in self.services: From e2b277c15265e608084445530059719d4b0446ae Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 13:55:52 +0200 Subject: [PATCH 06/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needgantt`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- sphinx_needs/data.py | 3 +-- sphinx_needs/diagrams_common.py | 29 ++++++++++++++++++++++++---- sphinx_needs/directives/needgantt.py | 5 ++--- sphinx_needs/filter_common.py | 19 +++++++++++++++++- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8cdeb8ef2..21e95e1be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,8 @@ namespace_packages = true [[tool.mypy.overrides]] module = [ "requests_file", - "sphinx_data_viewer.*" + "sphinx_data_viewer.*", + "sphinxcontrib.plantuml.*", ] ignore_missing_imports = true @@ -104,7 +105,6 @@ module = [ 'sphinx_needs.directives.needextract', 'sphinx_needs.directives.needfilter', 'sphinx_needs.directives.needflow', - 'sphinx_needs.directives.needgantt', 'sphinx_needs.directives.needlist', 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needsequence', diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index 62f42b232..c19f3732f 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -246,13 +246,12 @@ class NeedsExtendType(NeedsBaseDataType): class NeedsFilteredBaseType(NeedsBaseDataType): """A base type for all filtered data.""" - # Filter attributes status: list[str] tags: list[str] types: list[str] filter: None | str sort_by: None | str - filter_code: str + filter_code: list[str] filter_func: None | str export_id: str diff --git a/sphinx_needs/diagrams_common.py b/sphinx_needs/diagrams_common.py index ecef357a4..39ce8157f 100644 --- a/sphinx_needs/diagrams_common.py +++ b/sphinx_needs/diagrams_common.py @@ -6,7 +6,7 @@ import html import os import textwrap -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse from docutils import nodes @@ -15,13 +15,34 @@ from sphinx.util.docutils import SphinxDirective from sphinx_needs.config import NeedsSphinxConfig +from sphinx_needs.data import NeedsFilteredBaseType from sphinx_needs.errors import NoUri from sphinx_needs.logging import get_logger from sphinx_needs.utils import get_scale, split_link_types, unwrap +try: + from typing import TypedDict +except ImportError: + # introduced in python 3.8 + from typing_extensions import TypedDict + logger = get_logger(__name__) +class DiagramAttributesType(TypedDict): + show_legend: bool + show_filters: bool + show_link_names: bool + link_types: List[str] + config: str + config_names: str + scale: str + highlight: str + align: Optional[str] + debug: bool + caption: Optional[str] + + class DiagramBase(SphinxDirective): has_content = True @@ -44,7 +65,7 @@ def create_target(self, target_name: str) -> Tuple[int, str, nodes.target]: return id, targetid, targetnode - def collect_diagram_attributes(self) -> Dict[str, Any]: + def collect_diagram_attributes(self) -> DiagramAttributesType: location = (self.env.docname, self.lineno) link_types = split_link_types(self.options.get("link_types", "links"), location) @@ -58,7 +79,7 @@ def collect_diagram_attributes(self) -> Dict[str, Any]: if config_name and config_name in needs_config.flow_configs: configs.append(needs_config.flow_configs[config_name]) - collected_diagram_options = { + collected_diagram_options: DiagramAttributesType = { "show_legend": "show_legend" in self.options, "show_filters": "show_filters" in self.options, "show_link_names": "show_link_names" in self.options, @@ -104,7 +125,7 @@ def add_config(config: str) -> str: return uml -def get_filter_para(node_element: nodes.Element) -> nodes.paragraph: +def get_filter_para(node_element: NeedsFilteredBaseType) -> nodes.paragraph: """Return paragraph containing the used filter description""" para = nodes.paragraph() filter_text = "Used filter:" diff --git a/sphinx_needs/directives/needgantt.py b/sphinx_needs/directives/needgantt.py index fd00c95f2..9c51e49ca 100644 --- a/sphinx_needs/directives/needgantt.py +++ b/sphinx_needs/directives/needgantt.py @@ -244,8 +244,7 @@ def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, fo is_milestone = filter_single_need(app, need, current_needgantt["milestone_filter"]) else: is_milestone = False - constrain_types = ["starts_with_links", "starts_after_links", "ends_with_links"] - for con_type in constrain_types: + for con_type in ("starts_with_links", "starts_after_links", "ends_with_links"): if is_milestone: keyword = "happens" elif con_type in ["starts_with_links", "starts_after_links"]: @@ -258,7 +257,7 @@ def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, fo else: start_end_sync = "start" - for link_type in current_needgantt[con_type]: + for link_type in current_needgantt[con_type]: # type: ignore[literal-required] start_with_links = need[link_type] for start_with_link in start_with_links: start_need = all_needs_dict[start_with_link] diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index 298ab076d..13744a2dd 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -19,6 +19,23 @@ from sphinx_needs.utils import check_and_get_external_filter_func from sphinx_needs.utils import logger as log +try: + from typing import TypedDict +except ImportError: + # introduced in python 3.8 + from typing_extensions import TypedDict + + +class FilterAttributesType(TypedDict): + status: List[str] + tags: List[str] + types: List[str] + filter: str + sort_by: str + filter_code: List[str] + filter_func: str + export_id: str + class FilterBase(SphinxDirective): has_content = True @@ -33,7 +50,7 @@ class FilterBase(SphinxDirective): "export_id": directives.unchanged, } - def collect_filter_attributes(self) -> Dict[str, Any]: + def collect_filter_attributes(self) -> FilterAttributesType: tags = str(self.options.get("tags", "")) if tags: tags = [tag.strip() for tag in re.split(";|,", tags) if len(tag) > 0] From e45bc1abf907580fb7df042ceb6285225b4c7491 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 14:20:23 +0200 Subject: [PATCH 07/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needsequence`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/directives/needsequence.py | 32 +++++++++++++++---------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 21e95e1be..8d5e9e290 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,6 @@ module = [ 'sphinx_needs.directives.needflow', 'sphinx_needs.directives.needlist', 'sphinx_needs.directives.needpie', - 'sphinx_needs.directives.needsequence', 'sphinx_needs.directives.needtable', 'sphinx_needs.directives.needuml', 'sphinx_needs.directives.utils', diff --git a/sphinx_needs/directives/needsequence.py b/sphinx_needs/directives/needsequence.py index 57a017bdb..c98e885c8 100644 --- a/sphinx_needs/directives/needsequence.py +++ b/sphinx_needs/directives/needsequence.py @@ -1,6 +1,6 @@ import os import re -from typing import List, Sequence +from typing import Any, Dict, List, Optional, Sequence, Tuple from docutils import nodes from docutils.parsers.rst import directives @@ -10,7 +10,7 @@ ) from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.data import NeedsInfoType, SphinxNeedsData from sphinx_needs.diagrams_common import ( DiagramBase, add_config, @@ -83,7 +83,7 @@ def process_needsequence( needs_config = NeedsSphinxConfig(env.config) include_needs = needs_config.include_needs - link_types = needs_config.extra_links + link_type_names = [link["option"].upper() for link in needs_config.extra_links] needs_types = needs_config.types # NEEDSEQUENCE @@ -105,11 +105,11 @@ def process_needsequence( option_link_types = [link.upper() for link in current_needsequence["link_types"]] for lt in option_link_types: - if lt not in [link["option"].upper() for link in link_types]: + if lt not in link_type_names: logger.warning( "Unknown link type {link_type} in needsequence {flow}. Allowed values:" " {link_types} [needs]".format( - link_type=lt, flow=current_needsequence["target_id"], link_types=",".join(link_types) + link_type=lt, flow=current_needsequence["target_id"], link_types=",".join(link_type_names) ), type="needs", ) @@ -138,10 +138,11 @@ def process_needsequence( start_needs_id = [x.strip() for x in re.split(";|,", current_needsequence["start"])] if len(start_needs_id) == 0: - raise NeedsequenceDirective( + # TODO this should be a warning (and not tested) + raise NeedSequenceException( "No start-id set for needsequence" - " File {}" - ":{}".format({current_needsequence["docname"]}, current_needsequence["lineno"]) + f" docname {current_needsequence['docname']}" + f":{current_needsequence['lineno']}" ) puml_node["uml"] += "\n' Nodes definition \n\n" @@ -231,14 +232,21 @@ def process_needsequence( node.replace_self(content) -def get_message_needs(app: Sphinx, sender, link_types, all_needs_dict, tracked_receivers=None, filter=None): - msg_needs = [] +def get_message_needs( + app: Sphinx, + sender: NeedsInfoType, + link_types: List[str], + all_needs_dict: Dict[str, NeedsInfoType], + tracked_receivers: Optional[List[str]] = None, + filter: Optional[str] = None, +) -> Tuple[Dict[str, Dict[str, Any]], str, str]: + msg_needs: List[Dict[str, Any]] = [] if tracked_receivers is None: tracked_receivers = [] for link_type in link_types: - msg_needs += [all_needs_dict[x] for x in sender[link_type]] + msg_needs += [all_needs_dict[x] for x in sender[link_type]] # type: ignore - messages = {} + messages: Dict[str, Dict[str, Any]] = {} p_string = "" c_string = "" for msg_need in msg_needs: From 969363c9bf3c6deab6fdcdefca4074ad6880d3e6 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 14:26:43 +0200 Subject: [PATCH 08/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needlist`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/directives/needlist.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d5e9e290..436886e93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,6 @@ module = [ 'sphinx_needs.directives.needextract', 'sphinx_needs.directives.needfilter', 'sphinx_needs.directives.needflow', - 'sphinx_needs.directives.needlist', 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needtable', 'sphinx_needs.directives.needuml', diff --git a/sphinx_needs/directives/needlist.py b/sphinx_needs/directives/needlist.py index 69174e020..475cf7eee 100644 --- a/sphinx_needs/directives/needlist.py +++ b/sphinx_needs/directives/needlist.py @@ -34,7 +34,7 @@ class NeedlistDirective(FilterBase): } # Update the options_spec with values defined in the FilterBase class - option_spec.update(FilterBase.base_option_spec) # type: ignore[arg-type] + option_spec.update(FilterBase.base_option_spec) def run(self) -> Sequence[nodes.Node]: env = self.env @@ -83,9 +83,8 @@ def process_needlist(app: Sphinx, doctree: nodes.document, fromdocname: str, fou id = node.attributes["ids"][0] current_needfilter = SphinxNeedsData(env).get_or_create_lists()[id] - all_needs = SphinxNeedsData(env).get_or_create_needs() content: List[nodes.Node] = [] - all_needs = list(all_needs.values()) + all_needs = list(SphinxNeedsData(env).get_or_create_needs().values()) found_needs = process_filters(app, all_needs, current_needfilter) line_block = nodes.line_block() From f8c4d0001a7fc376a1f4edf33de335cc35e13d76 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 15:19:24 +0200 Subject: [PATCH 09/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needtable`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/api/need.py | 1 - sphinx_needs/data.py | 1 + sphinx_needs/diagrams_common.py | 1 - sphinx_needs/directives/needextract.py | 7 ----- sphinx_needs/directives/needfilter.py | 5 +--- sphinx_needs/directives/needgantt.py | 6 ++-- sphinx_needs/directives/needlist.py | 8 ++--- sphinx_needs/directives/needtable.py | 41 +++++++++++++++----------- sphinx_needs/filter_common.py | 14 +++------ sphinx_needs/functions/functions.py | 2 +- sphinx_needs/needsfile.py | 2 -- sphinx_needs/roles/need_incoming.py | 1 - sphinx_needs/utils.py | 12 ++++---- 14 files changed, 42 insertions(+), 60 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 436886e93..bfe17e46f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ module = [ 'sphinx_needs.directives.needfilter', 'sphinx_needs.directives.needflow', 'sphinx_needs.directives.needpie', - 'sphinx_needs.directives.needtable', 'sphinx_needs.directives.needuml', 'sphinx_needs.directives.utils', 'sphinx_needs.external_needs', diff --git a/sphinx_needs/api/need.py b/sphinx_needs/api/need.py index e8af90b97..989487aaf 100644 --- a/sphinx_needs/api/need.py +++ b/sphinx_needs/api/need.py @@ -309,7 +309,6 @@ def run(): "docname": docname, "doctype": doctype, "lineno": lineno, - # "target_node": target_node, "target_id": need_id, "external_url": external_url, "content_node": None, # gets set after rst parsing diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index c19f3732f..4a2a05b2b 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -362,6 +362,7 @@ class NeedsTableType(NeedsFilteredBaseType): caption: None | str classes: list[str] columns: list[tuple[str, str]] + """List of (name, title)""" colwidths: list[int] style: str style_row: str diff --git a/sphinx_needs/diagrams_common.py b/sphinx_needs/diagrams_common.py index 39ce8157f..c34f22af6 100644 --- a/sphinx_needs/diagrams_common.py +++ b/sphinx_needs/diagrams_common.py @@ -179,7 +179,6 @@ def calculate_link(app: Sphinx, need_info: Dict[str, Any], _fromdocname: str) -> # only need to add ../ or ..\ to get out of the image folder link = ".." + os.path.sep + need_info["external_url"] else: - # link = "../" + builder.get_target_uri(need_info["docname"]) + "#" + need_info["target_node"]["refid"] link = "../" + builder.get_target_uri(need_info["docname"]) + "#" + need_info["target_id"] if need_info["is_part"]: link = f"{link}.{need_info['id']}" diff --git a/sphinx_needs/directives/needextract.py b/sphinx_needs/directives/needextract.py index 98f498005..d4cc0406e 100644 --- a/sphinx_needs/directives/needextract.py +++ b/sphinx_needs/directives/needextract.py @@ -111,13 +111,6 @@ def process_needextract( found_needs = process_filters(app, all_needs.values(), current_needextract) for need_info in found_needs: - # if "is_target" is True: - # extract_target_node = current_needextract['target_node'] - # extract_target_node[ids=[need_info["id"]]] - # - # # Original need id replacement (needextract-{docname}-{id}) - # need_info['target_node']['ids'] = [f"replaced_{need['id']}"] - # filter out need_part from found_needs, in order to generate # copies of filtered needs with custom layout and style if need_info["is_need"] and not need_info["is_part"]: diff --git a/sphinx_needs/directives/needfilter.py b/sphinx_needs/directives/needfilter.py index ca424b5cf..09d52c586 100644 --- a/sphinx_needs/directives/needfilter.py +++ b/sphinx_needs/directives/needfilter.py @@ -161,10 +161,7 @@ def process_needfilters( line_block = nodes.line_block() for need_info in found_needs: - if "target_node" in need_info: - target_id = need_info["target_node"]["refid"] - else: - target_id = need_info["target_id"] + target_id = need_info["target_id"] if current_needfilter["layout"] == "list": para = nodes.line() diff --git a/sphinx_needs/directives/needgantt.py b/sphinx_needs/directives/needgantt.py index 9c51e49ca..29f6fda4c 100644 --- a/sphinx_needs/directives/needgantt.py +++ b/sphinx_needs/directives/needgantt.py @@ -204,9 +204,9 @@ def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, fo gantt_element = "[{}] as [{}] lasts 0 days\n".format(need["title"], need["id"]) else: # Normal gantt element handling duration_option = current_needgantt["duration_option"] - duration = need[duration_option] + duration = need[duration_option] # type: ignore[literal-required] complete_option = current_needgantt["completion_option"] - complete = need[complete_option] + complete = need[complete_option] # type: ignore[literal-required] if not (duration and duration.isdigit()): logger.warning( "Duration not set or invalid for needgantt chart. " @@ -258,7 +258,7 @@ def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, fo start_end_sync = "start" for link_type in current_needgantt[con_type]: # type: ignore[literal-required] - start_with_links = need[link_type] + start_with_links = need[link_type] # type: ignore[literal-required] for start_with_link in start_with_links: start_need = all_needs_dict[start_with_link] gantt_constraint = "[{}] {} at [{}]'s " "{}\n".format( diff --git a/sphinx_needs/directives/needlist.py b/sphinx_needs/directives/needlist.py index 475cf7eee..f35f4a904 100644 --- a/sphinx_needs/directives/needlist.py +++ b/sphinx_needs/directives/needlist.py @@ -107,6 +107,7 @@ def process_needlist(app: Sphinx, doctree: nodes.document, fromdocname: str, fou if need_info["hide"]: para += title elif need_info["is_external"]: + assert need_info["external_url"] is not None, "External need without URL" ref = nodes.reference("", "") ref["refuri"] = check_and_calc_base_url_rel_path(need_info["external_url"], fromdocname) @@ -115,12 +116,7 @@ def process_needlist(app: Sphinx, doctree: nodes.document, fromdocname: str, fou ref.append(title) para += ref else: - # target_node should not be stored, but it may be still the case - if "target_node" in need_info: - target_id = need_info["target_node"]["refid"] - else: - target_id = need_info["target_id"] - + target_id = need_info["target_id"] ref = nodes.reference("", "") ref["refdocname"] = need_info["docname"] ref["refuri"] = builder.get_relative_uri(fromdocname, need_info["docname"]) diff --git a/sphinx_needs/directives/needtable.py b/sphinx_needs/directives/needtable.py index 1e974bb6a..c7c890086 100644 --- a/sphinx_needs/directives/needtable.py +++ b/sphinx_needs/directives/needtable.py @@ -1,5 +1,5 @@ import re -from typing import List, Sequence +from typing import Any, Callable, List, Sequence from docutils import nodes from docutils.parsers.rst import directives @@ -7,7 +7,7 @@ from sphinx_needs.api.exceptions import NeedsInvalidException from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.data import NeedsInfoType, SphinxNeedsData from sphinx_needs.debug import measure_time from sphinx_needs.directives.utils import ( get_option_list, @@ -53,13 +53,15 @@ def run(self) -> Sequence[nodes.Node]: targetid = "needtable-{docname}-{id}".format(docname=env.docname, id=env.new_serialno("needtable")) targetnode = nodes.target("", "", ids=[targetid]) - columns = str(self.options.get("columns", "")) - if len(columns) == 0: - columns = NeedsSphinxConfig(env.app.config).table_columns - if isinstance(columns, str): - columns = [col.strip() for col in re.split(";|,", columns)] + columns_str = str(self.options.get("columns", "")) + if len(columns_str) == 0: + columns_str = NeedsSphinxConfig(env.app.config).table_columns + if isinstance(columns_str, str): + _columns = [col.strip() for col in re.split(";|,", columns_str)] + else: + _columns = columns_str - columns = [get_title(col) for col in columns] + columns = [get_title(col) for col in _columns] colwidths = str(self.options.get("colwidths", "")) colwidths_list = [] @@ -213,24 +215,25 @@ def process_needtables( except Exception as e: raise e - def get_sorter(key): + def get_sorter(key: str) -> Callable[[NeedsInfoType], Any]: """ Returns a sort-function for a given need-key. :param key: key of need object as string :return: function to use in sort(key=x) """ - def sort(need): + def sort(need: NeedsInfoType) -> Any: """ Returns a given value of need, which is used for list sorting. :param need: need-element, which gets sort :return: value of need """ - if isinstance(need[key], str): + value = need[key] # type: ignore[literal-required] + if isinstance(value, str): # if we filter for string (e.g. id) everything should be lowercase. # Otherwise, "Z" will be above "a" - return need[key].lower() - return need[key] + return value.lower() + return value return sort @@ -246,7 +249,9 @@ def sort(need): prefix = "" else: row = nodes.row(classes=["need_part", style_row]) - temp_need["id"] = temp_need["id_complete"] + temp_need["id"] = temp_need[ + "id_complete" # type: ignore[typeddict-item] # TODO this is set in prepare_need_list + ] prefix = needs_config.part_prefix temp_need["title"] = temp_need["content"] @@ -279,10 +284,10 @@ def sort(need): for part in need_info["parts"].values(): # update the part with all information from its parent # this is required to make ID links work - temp_part = part.copy() # The dict has to be manipulated, so that row_col_maker() can be used - temp_part = {**need_info, **temp_part} - temp_part["id_complete"] = f"{need_info['id']}.{temp_part['id']}" - temp_part["id_parent"] = need_info["id"] + # The dict has to be manipulated, so that row_col_maker() can be used + temp_part: NeedsInfoType = {**need_info, **part.copy()} # type: ignore[typeddict-unknown-key] + temp_part["id_complete"] = f"{need_info['id']}.{temp_part['id']}" # type: ignore[typeddict-unknown-key] + temp_part["id_parent"] = need_info["id"] # type: ignore[typeddict-unknown-key] temp_part["docname"] = need_info["docname"] row = nodes.row(classes=["need_part"]) diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index 13744a2dd..8a1603e8a 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -86,7 +86,7 @@ def collect_filter_attributes(self) -> FilterAttributesType: def process_filters( app: Sphinx, all_needs: List[NeedsInfoType], filter_data: NeedsFilteredBaseType, include_external: bool = True -): +) -> List[NeedsInfoType]: """ Filters all needs with given configuration. Used by needlist, needtable and needflow. @@ -205,13 +205,7 @@ def process_filters( filter_list = SphinxNeedsData(env).get_or_create_filters() found_needs_ids = [need["id_complete"] for need in found_needs] - if "target_node" in filter_data: - target_id = filter_data["target_node"]["refid"] - else: - target_id = filter_data["target_id"] - - filter_list[target_id] = { - # "target_node": current_needlist["target_node"], + filter_list[filter_data["target_id"]] = { "filter": filter_data["filter"] or "", "status": filter_data["status"], "tags": filter_data["tags"], @@ -243,9 +237,9 @@ def prepare_need_list(need_list: List[NeedsInfoType]) -> List[NeedsInfoType]: # Be sure extra attributes, which makes only sense for need_parts, are also available on # need level so that no KeyError gets raised, if search/filter get executed on needs with a need-part argument. - if "id_parent" not in need.keys(): + if "id_parent" not in need: need["id_parent"] = need["id"] - if "id_complete" not in need.keys(): + if "id_complete" not in need: need["id_complete"] = need["id"] return all_needs_incl_parts diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index 60f95071e..9c15aa03d 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -174,7 +174,7 @@ def resolve_dynamic_values(env: BuildEnvironment): needs = data.get_or_create_needs() for need in needs.values(): for need_option in need: - if need_option in ["docname", "lineno", "target_node", "content", "content_node", "content_id"]: + if need_option in ["docname", "lineno", "content", "content_node", "content_id"]: # dynamic values in this data are not allowed. continue if not isinstance(need[need_option], (list, set)): diff --git a/sphinx_needs/needsfile.py b/sphinx_needs/needsfile.py index a7786b1bc..6bd126c9e 100644 --- a/sphinx_needs/needsfile.py +++ b/sphinx_needs/needsfile.py @@ -22,7 +22,6 @@ class NeedsList: "links_back", "type_color", "hide_status", - "target_node", "hide", "type_prefix", "lineno", @@ -37,7 +36,6 @@ class NeedsList: "links_back", "type_color", "hide_status", - "target_node", "hide", "type_prefix", "lineno", diff --git a/sphinx_needs/roles/need_incoming.py b/sphinx_needs/roles/need_incoming.py index de9e6f564..7837dcb18 100644 --- a/sphinx_needs/roles/need_incoming.py +++ b/sphinx_needs/roles/need_incoming.py @@ -59,7 +59,6 @@ def process_need_incoming( builder, fromdocname, target_need["docname"], - # target_need["target_node"]["refid"], target_need["target_id"], node_need_backref[0].deepcopy(), node_need_backref["reftarget"], diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index 9f3194a52..b7d8a4db4 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -27,7 +27,6 @@ "docname", "doctype", "lineno", - "target_node", "refid", "content", "pre_content", @@ -87,12 +86,12 @@ def row_col_maker( app: Sphinx, fromdocname: str, all_needs: Dict[str, NeedsInfoType], - need_info, - need_key, + need_info: NeedsInfoType, + need_key: str, make_ref: bool = False, ref_lookup: bool = False, prefix: str = "", -): +) -> nodes.entry: """ Creates and returns a column. @@ -254,7 +253,10 @@ def import_prefix_link_edit(needs: Dict[str, Any], id_prefix: str, needs_extra_l need["description"] = need["description"].replace(id, "".join([id_prefix, id])) -def profile(keyword: str): +FuncT = TypeVar("FuncT") + + +def profile(keyword: str) -> Callable[[FuncT], FuncT]: """ Activate profiling for a specific function. From 741f565bd2817e3b35afbb2ea13d4e83f4e99fba Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 17:04:54 +0200 Subject: [PATCH 10/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.roles.need=5Fincoming`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 -- sphinx_needs/config.py | 2 +- sphinx_needs/data.py | 7 +++++-- sphinx_needs/layout.py | 2 +- sphinx_needs/roles/need_incoming.py | 9 +++++---- sphinx_needs/roles/need_outgoing.py | 3 ++- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bfe17e46f..4834aea31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,8 +114,6 @@ module = [ 'sphinx_needs.functions.functions', 'sphinx_needs.layout', 'sphinx_needs.needsfile', - 'sphinx_needs.roles.need_incoming', - 'sphinx_needs.roles.need_outgoing', 'sphinx_needs.roles.need_part', 'sphinx_needs.roles.need_ref', 'sphinx_needs.services.github', diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index ba4d38e50..bb0c4ddc8 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -171,7 +171,7 @@ def __setattr__(self, name: str, value: Any) -> None: extra_links: list[dict[str, Any]] = field(default_factory=list, metadata={"rebuild": "html", "types": ()}) """List of additional links, which can be used by setting related option Values needed for each new link: - * name (will also be the option name) + * option (will also be the option name) * incoming * copy_link (copy to common links data. Default: True) * color (used for needflow. Default: #000000) diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index 4a2a05b2b..ec9108087 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -71,10 +71,10 @@ class NeedsPartType(TypedDict): """Content of the part.""" document: str """docname where the part is defined.""" - links_back: list[str] - """List of need IDs, which are referencing this part.""" links: list[str] """List of need IDs, which are referenced by this part.""" + links_back: list[str] + """List of need IDs, which are referencing this part.""" class NeedsInfoType(NeedsBaseDataType): @@ -149,11 +149,14 @@ class NeedsInfoType(NeedsBaseDataType): # link information links: list[str] """List of need IDs, which are referenced by this need.""" + links_back: list[str] + """List of need IDs, which are referencing this need.""" # TODO there is a lot more dynamically added link information; # for each item in needs_extra_links config, # you end up with a key named by the "option" field, # and then another key named by the "option" field + "_back" # these all have value type `list[str]` + # back links are all set in process_need_nodes (-> create_back_links) transform # constraints information # set in process_need_nodes (-> process_constraints) transform diff --git a/sphinx_needs/layout.py b/sphinx_needs/layout.py index 51b1af75c..cf47c9953 100644 --- a/sphinx_needs/layout.py +++ b/sphinx_needs/layout.py @@ -597,7 +597,7 @@ def meta_all( show_empty: bool = False, ): """ - ``meta_all()`` excludes by default the output of: ``docname``, ``lineno``, ``target_node``, ``refid``, + ``meta_all()`` excludes by default the output of: ``docname``, ``lineno``, ``refid``, ``content``, ``collapse``, ``parts``, ``id_parent``, ``id_complete``, ``title``, ``full_title``, ``is_part``, ``is_need``, ``type_prefix``, ``type_color``, ``type_style``, ``type``, ``type_name``, ``id``, diff --git a/sphinx_needs/roles/need_incoming.py b/sphinx_needs/roles/need_incoming.py index 7837dcb18..e32900d10 100644 --- a/sphinx_needs/roles/need_incoming.py +++ b/sphinx_needs/roles/need_incoming.py @@ -7,10 +7,10 @@ from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import SphinxNeedsData from sphinx_needs.errors import NoUri -from sphinx_needs.utils import check_and_calc_base_url_rel_path, unwrap +from sphinx_needs.utils import check_and_calc_base_url_rel_path, logger, unwrap -class NeedIncoming(nodes.Inline, nodes.Element): +class NeedIncoming(nodes.Inline, nodes.Element): # type: ignore pass @@ -29,7 +29,7 @@ def process_need_incoming( # Let's check if NeedIncoming shall follow a specific link type if "link_type" in node_need_backref.attributes: - links_back = ref_need[node_need_backref.attributes["link_type"]] + links_back = ref_need[node_need_backref.attributes["link_type"]] # type: ignore[literal-required] # if not, follow back to default links else: links_back = ref_need["links_back"] @@ -64,6 +64,7 @@ def process_need_incoming( node_need_backref["reftarget"], ) else: + assert target_need["external_url"] is not None, "External URL must not be set" new_node_ref = nodes.reference(target_need["id"], target_need["id"]) new_node_ref["refuri"] = check_and_calc_base_url_rel_path( target_need["external_url"], fromdocname @@ -81,7 +82,7 @@ def process_need_incoming( pass else: - env.warn_node("need %s not found [needs]" % node_need_backref["reftarget"], node_need_backref) + logger.warning(f"need {node_need_backref['reftarget']} not found [needs]", location=node_need_backref) if len(node_link_container.children) == 0: node_link_container += nodes.Text("None") diff --git a/sphinx_needs/roles/need_outgoing.py b/sphinx_needs/roles/need_outgoing.py index e20354a1e..38a05b8cd 100644 --- a/sphinx_needs/roles/need_outgoing.py +++ b/sphinx_needs/roles/need_outgoing.py @@ -32,7 +32,7 @@ def process_need_outgoing( # Let's check if NeedIncoming shall follow a specific link type if "link_type" in node_need_ref.attributes: - links = ref_need[node_need_ref.attributes["link_type"]] + links = ref_need[node_need_ref.attributes["link_type"]] # type: ignore[literal-required] link_type = node_need_ref.attributes["link_type"] # if not, follow back to default links else: @@ -86,6 +86,7 @@ def process_need_outgoing( node_need_ref["reftarget"], ) else: + assert target_need["external_url"] is not None, "External URL must be set" new_node_ref = nodes.reference(target_need["id"], target_need["id"]) new_node_ref["refuri"] = check_and_calc_base_url_rel_path( target_need["external_url"], fromdocname From a19f4b05a933eae2138a7e3bd0ca809b87c045df Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 17:11:19 +0200 Subject: [PATCH 11/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.roles.need=5Fpart`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/roles/need_part.py | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4834aea31..295effed8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,7 +114,6 @@ module = [ 'sphinx_needs.functions.functions', 'sphinx_needs.layout', 'sphinx_needs.needsfile', - 'sphinx_needs.roles.need_part', 'sphinx_needs.roles.need_ref', 'sphinx_needs.services.github', 'sphinx_needs.utils', diff --git a/sphinx_needs/roles/need_part.py b/sphinx_needs/roles/need_part.py index f9b37e07a..0246069ce 100644 --- a/sphinx_needs/roles/need_part.py +++ b/sphinx_needs/roles/need_part.py @@ -8,19 +8,20 @@ """ import hashlib import re -from typing import List +from typing import List, cast from docutils import nodes from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment +from sphinx_needs.data import NeedsInfoType from sphinx_needs.logging import get_logger from sphinx_needs.utils import unwrap log = get_logger(__name__) -class NeedPart(nodes.Inline, nodes.Element): +class NeedPart(nodes.Inline, nodes.Element): # type: ignore pass @@ -31,11 +32,11 @@ def process_need_part(app: Sphinx, doctree: nodes.document, fromdocname: str, fo part_pattern = re.compile(r"\(([\w-]+)\)(.*)") -def update_need_with_parts(env: BuildEnvironment, need, part_nodes: List[NeedPart]) -> None: +def update_need_with_parts(env: BuildEnvironment, need: NeedsInfoType, part_nodes: List[NeedPart]) -> None: app = unwrap(env.app) builder = unwrap(app.builder) for part_node in part_nodes: - content = part_node.children[0].children[0] # ->inline->Text + content = cast(str, part_node.children[0].children[0]) # ->inline->Text result = part_pattern.match(content) if result: inline_id = result.group(1) @@ -85,7 +86,7 @@ def update_need_with_parts(env: BuildEnvironment, need, part_nodes: List[NeedPar part_node.append(node_need_part_line) -def find_parts(node: nodes.Element) -> List[NeedPart]: +def find_parts(node: nodes.Node) -> List[NeedPart]: found_nodes = [] for child in node.children: if isinstance(child, NeedPart): From f3725a0cef8c1fac0bafc0f9d7de266974494f6c Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 17:20:01 +0200 Subject: [PATCH 12/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.needsfile`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 1 + pyproject.toml | 1 - sphinx_needs/needsfile.py | 37 +++++++++++++++++-------------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f237a4349..1031bec26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,7 @@ repos: additional_dependencies: - sphinx==6 - types-docutils + - types-jsonschema - types-requests - repo: https://github.com/python-poetry/poetry diff --git a/pyproject.toml b/pyproject.toml index 295effed8..86fad15bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,6 @@ module = [ 'sphinx_needs.functions.common', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', - 'sphinx_needs.needsfile', 'sphinx_needs.roles.need_ref', 'sphinx_needs.services.github', 'sphinx_needs.utils', diff --git a/sphinx_needs/needsfile.py b/sphinx_needs/needsfile.py index 6bd126c9e..625a4a657 100644 --- a/sphinx_needs/needsfile.py +++ b/sphinx_needs/needsfile.py @@ -10,8 +10,9 @@ from typing import Any, List from jsonschema import Draft7Validator +from sphinx.config import Config -from sphinx_needs.data import NeedsFilterType +from sphinx_needs.data import NeedsFilterType, NeedsInfoType from sphinx_needs.logging import get_logger log = get_logger(__name__) @@ -46,7 +47,7 @@ class NeedsList: "content_node", } - def __init__(self, config, outdir, confdir) -> None: + def __init__(self, config: Config, outdir: str, confdir: str) -> None: self.config = config self.outdir = outdir self.confdir = confdir @@ -60,7 +61,7 @@ def __init__(self, config, outdir, confdir) -> None: } self.log = log - def update_or_add_version(self, version) -> None: + def update_or_add_version(self, version: str) -> None: if version not in self.needs_list["versions"].keys(): self.needs_list["versions"][version] = { "created": "", @@ -75,16 +76,16 @@ def update_or_add_version(self, version) -> None: self.needs_list["versions"][version]["created"] = datetime.now().isoformat() - def add_need(self, version, need_info) -> None: + def add_need(self, version: str, need_info: NeedsInfoType) -> None: self.update_or_add_version(version) - writable_needs = {key: need_info[key] for key in need_info if key not in self.JSON_KEY_EXCLUSIONS_NEEDS} + writable_needs = {key: need_info[key] for key in need_info if key not in self.JSON_KEY_EXCLUSIONS_NEEDS} # type: ignore[literal-required] writable_needs["description"] = need_info["content"] self.needs_list["versions"][version]["needs"][need_info["id"]] = writable_needs self.needs_list["versions"][version]["needs_amount"] = len(self.needs_list["versions"][version]["needs"]) - def add_filter(self, version, need_filter: NeedsFilterType) -> None: + def add_filter(self, version: str, need_filter: NeedsFilterType) -> None: self.update_or_add_version(version) - writable_filters = {key: need_filter[key] for key in need_filter if key not in self.JSON_KEY_EXCLUSIONS_FILTERS} + writable_filters = {key: need_filter[key] for key in need_filter if key not in self.JSON_KEY_EXCLUSIONS_FILTERS} # type: ignore[literal-required] self.needs_list["versions"][version]["filters"][need_filter["export_id"].upper()] = writable_filters self.needs_list["versions"][version]["filters_amount"] = len(self.needs_list["versions"][version]["filters"]) @@ -98,13 +99,10 @@ def write_json(self, needs_file: str = "needs.json") -> None: self.needs_list["current_version"] = self.current_version self.needs_list["project"] = self.project - needs_json = json.dumps(self.needs_list, indent=4, sort_keys=True) - file = os.path.join(self.outdir, needs_file) + with open(os.path.join(self.outdir, needs_file), "w") as f: + json.dump(self.needs_list, f, indent=4, sort_keys=True) - with open(file, "w") as f: - f.write(needs_json) - - def load_json(self, file) -> None: + def load_json(self, file: str) -> None: if not os.path.isabs(file): file = os.path.join(self.confdir, file) @@ -120,13 +118,12 @@ def load_json(self, file) -> None: self.log.info(f' {error.message} -> {".".join(error.path)}') with open(file) as needs_file: - needs_file_content = needs_file.read() - try: - needs_list = json.loads(needs_file_content) - except json.JSONDecodeError: - self.log.warning(f"Could not decode json file {file} [needs]", type="needs") - else: - self.needs_list = needs_list + try: + needs_list = json.load(needs_file) + except json.JSONDecodeError: + self.log.warning(f"Could not decode json file {file} [needs]", type="needs") + else: + self.needs_list = needs_list self.log.debug(f"needs.json file loaded: {file}") From 2cd419f31b42c5fc2599e69147fb1346c1c0e43e Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 17:42:21 +0200 Subject: [PATCH 13/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.functions.common`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/functions/__init__.py | 1 + sphinx_needs/functions/common.py | 81 +++++++++++++++++++----------- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 86fad15bc..772aa2190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,6 @@ module = [ 'sphinx_needs.directives.utils', 'sphinx_needs.external_needs', 'sphinx_needs.filter_common', - 'sphinx_needs.functions.common', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', 'sphinx_needs.roles.need_ref', diff --git a/sphinx_needs/functions/__init__.py b/sphinx_needs/functions/__init__.py index e3bb471bd..92559b4ed 100644 --- a/sphinx_needs/functions/__init__.py +++ b/sphinx_needs/functions/__init__.py @@ -17,6 +17,7 @@ resolve_variants_options, ) +# TODO better type signature (Sphinx, NeedsInfoType, Dict[str, NeedsInfoType], *args, **kwargs) NEEDS_COMMON_FUNCTIONS: List[Callable[..., Any]] = [ test, echo, diff --git a/sphinx_needs/functions/common.py b/sphinx_needs/functions/common.py index d06941007..29638c123 100644 --- a/sphinx_needs/functions/common.py +++ b/sphinx_needs/functions/common.py @@ -6,16 +6,17 @@ import contextlib import re -from typing import List, Optional +from typing import Any, Dict, List, Optional from sphinx.application import Sphinx from sphinx_needs.api.exceptions import NeedsInvalidFilter +from sphinx_needs.data import NeedsInfoType from sphinx_needs.filter_common import filter_needs, filter_single_need from sphinx_needs.utils import logger -def test(app: Sphinx, need, needs, *args, **kwargs) -> str: +def test(app: Sphinx, need: NeedsInfoType, needs: Dict[str, NeedsInfoType], *args: Any, **kwargs: Any) -> str: """ Test function for dynamic functions in sphinx needs. @@ -36,7 +37,9 @@ def test(app: Sphinx, need, needs, *args, **kwargs) -> str: return f"Test output of need {need['id']}. args: {args}. kwargs: {kwargs}" -def echo(app: Sphinx, need, needs, text: str, *args, **kwargs) -> str: +def echo( + app: Sphinx, need: NeedsInfoType, needs: Dict[str, NeedsInfoType], text: str, *args: Any, **kwargs: Any +) -> str: """ .. versionadded:: 0.6.3 @@ -53,7 +56,16 @@ def echo(app: Sphinx, need, needs, text: str, *args, **kwargs) -> str: return text -def copy(app: Sphinx, need, needs, option, need_id=None, lower: bool = False, upper: bool = False, filter=None) -> str: +def copy( + app: Sphinx, + need: NeedsInfoType, + needs: Dict[str, NeedsInfoType], + option: str, + need_id: Optional[str] = None, + lower: bool = False, + upper: bool = False, + filter: Optional[str] = None, +) -> Any: """ Copies the value of one need option to another @@ -143,24 +155,28 @@ def copy(app: Sphinx, need, needs, option, need_id=None, lower: bool = False, up if result: need = result[0] + value = need[option] # type: ignore[literal-required] + + # TODO check if str? + if lower: - return need[option].lower() + return value.lower() if upper: - return need[option].upper() + return value.upper() - return need[option] + return value def check_linked_values( app: Sphinx, - need, - needs, - result, - search_option, - search_value, + need: NeedsInfoType, + needs: Dict[str, NeedsInfoType], + result: Any, + search_option: str, + search_value: Any, filter_string: Optional[str] = None, one_hit: bool = False, -): +) -> Any: """ Returns a specific value, if for all linked needs a given option has a given value. @@ -297,22 +313,31 @@ def check_linked_values( search_value = [search_value] for link in links: + need = needs[link] if filter_string: try: - if not filter_single_need(app, needs[link], filter_string): + if not filter_single_need(app, need, filter_string): continue except Exception as e: logger.warning(f"CheckLinkedValues: Filter {filter_string} not valid: Error: {e} [needs]", type="needs") - if not one_hit and needs[link][search_option] not in search_value: + need_value = need[search_option] # type: ignore[literal-required] + if not one_hit and need_value not in search_value: return None - elif one_hit and needs[link][search_option] in search_value: + elif one_hit and need_value in search_value: return result return result -def calc_sum(app: Sphinx, need, needs, option, filter=None, links_only: bool = False) -> float: +def calc_sum( + app: Sphinx, + need: NeedsInfoType, + needs: Dict[str, NeedsInfoType], + option: str, + filter: Optional[str] = None, + links_only: bool = False, +) -> float: """ Sums the values of a given option in filtered needs up to single number. @@ -392,12 +417,7 @@ def calc_sum(app: Sphinx, need, needs, option, filter=None, links_only: bool = F :return: A float number """ - if links_only: - check_needs = [] - for link in need["links"]: - check_needs.append(needs[link]) - else: - check_needs = needs.values() + check_needs = [needs[link] for link in need["links"]] if links_only else needs.values() calculated_sum = 0.0 @@ -412,12 +432,18 @@ def calc_sum(app: Sphinx, need, needs, option, filter=None, links_only: bool = F logger.warning(f"Given filter is not valid. Error: {ex} [needs]", type="needs") with contextlib.suppress(ValueError): - calculated_sum += float(check_need[option]) + calculated_sum += float(check_need[option]) # type: ignore[literal-required] return calculated_sum -def links_from_content(app: Sphinx, need, needs, need_id=None, filter=None) -> List[str]: +def links_from_content( + app: Sphinx, + need: NeedsInfoType, + needs: Dict[str, NeedsInfoType], + need_id: Optional[str] = None, + filter: Optional[str] = None, +) -> List[str]: """ Extracts links from content of a need. @@ -469,10 +495,7 @@ def links_from_content(app: Sphinx, need, needs, need_id=None, filter=None) -> L :param filter: :ref:`filter_string`, which a found need-link must pass. :return: List of linked need-ids in content """ - if need_id: - source_need = needs[need_id] - else: - source_need = need + source_need = needs[need_id] if need_id else need links = re.findall(r":need:`(\w+)`|:need:`.+\<(.+)\>`", source_need["content"]) raw_links = [] From af63ab3634716c6ffef9d16fcdee3709c85e384b Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 17:53:39 +0200 Subject: [PATCH 14/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.external=5Fneeds`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/external_needs.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 772aa2190..bfeb362c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,6 @@ module = [ 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needuml', 'sphinx_needs.directives.utils', - 'sphinx_needs.external_needs', 'sphinx_needs.filter_common', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', diff --git a/sphinx_needs/external_needs.py b/sphinx_needs/external_needs.py index b78b3eaf7..2c3af8c50 100644 --- a/sphinx_needs/external_needs.py +++ b/sphinx_needs/external_needs.py @@ -2,7 +2,7 @@ import os import requests -from jinja2 import BaseLoader, Environment +from jinja2 import Environment from requests_file import FileAdapter from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment @@ -95,7 +95,7 @@ def load_external_needs(app: Sphinx, env: BuildEnvironment, _docname: str) -> No if target_url: # render jinja content - mem_template = Environment(loader=BaseLoader).from_string(target_url) + mem_template = Environment().from_string(target_url) cal_target_url = mem_template.render(**{"need": need}) need_params["external_url"] = f'{source["base_url"]}/{cal_target_url}' else: From 6f5508fbb38df0e7a650d40f421b0b29e762844f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 18:58:30 +0200 Subject: [PATCH 15/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.filter=5Fcommon`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 - sphinx_needs/data.py | 11 ++++- sphinx_needs/directives/needtable.py | 4 +- sphinx_needs/filter_common.py | 73 +++++++++++++++------------- 4 files changed, 50 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bfeb362c4..a9bcf7c01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,13 +102,11 @@ module = [ 'sphinx_needs.directives.need', 'sphinx_needs.directives.needbar', 'sphinx_needs.directives.needextend', - 'sphinx_needs.directives.needextract', 'sphinx_needs.directives.needfilter', 'sphinx_needs.directives.needflow', 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needuml', 'sphinx_needs.directives.utils', - 'sphinx_needs.filter_common', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', 'sphinx_needs.roles.need_ref', diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index ec9108087..5c059dede 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -63,7 +63,6 @@ class NeedsPartType(TypedDict): id: str """ID of the part""" - # TODO are these necessary? i.e. its always a part, not a need is_part: bool is_need: bool @@ -129,7 +128,6 @@ class NeedsInfoType(NeedsBaseDataType): # parts information parts: dict[str, NeedsPartType] - # TODO are these necessary? i.e. its always a need, not a part is_need: bool is_part: bool @@ -213,6 +211,15 @@ class NeedsInfoType(NeedsBaseDataType): # - dynamic global options that can be set by needs_global_options config +class NeedsPartsInfoType(NeedsInfoType): + """Generated by prepare_need_list""" + + document: str + """docname where the part is defined.""" + id_parent: str + id_complete: str + + class NeedsBarType(NeedsBaseDataType): """Data for a single (matplotlib) bar diagram.""" diff --git a/sphinx_needs/directives/needtable.py b/sphinx_needs/directives/needtable.py index c7c890086..304ee48ce 100644 --- a/sphinx_needs/directives/needtable.py +++ b/sphinx_needs/directives/needtable.py @@ -249,9 +249,7 @@ def sort(need: NeedsInfoType) -> Any: prefix = "" else: row = nodes.row(classes=["need_part", style_row]) - temp_need["id"] = temp_need[ - "id_complete" # type: ignore[typeddict-item] # TODO this is set in prepare_need_list - ] + temp_need["id"] = temp_need["id_complete"] prefix = needs_config.part_prefix temp_need["title"] = temp_need["content"] diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index 8a1603e8a..0e8d22295 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -6,7 +6,7 @@ import copy import re from types import CodeType -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Iterable, List, Optional, TypeVar from docutils.parsers.rst import directives from sphinx.application import Sphinx @@ -14,7 +14,12 @@ from sphinx_needs.api.exceptions import NeedsInvalidFilter from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import NeedsFilteredBaseType, NeedsInfoType, SphinxNeedsData +from sphinx_needs.data import ( + NeedsFilteredBaseType, + NeedsInfoType, + NeedsPartsInfoType, + SphinxNeedsData, +) from sphinx_needs.debug import measure_time from sphinx_needs.utils import check_and_get_external_filter_func from sphinx_needs.utils import logger as log @@ -51,9 +56,8 @@ class FilterBase(SphinxDirective): } def collect_filter_attributes(self) -> FilterAttributesType: - tags = str(self.options.get("tags", "")) - if tags: - tags = [tag.strip() for tag in re.split(";|,", tags) if len(tag) > 0] + _tags = str(self.options.get("tags", "")) + tags = [tag.strip() for tag in re.split(";|,", _tags) if len(tag) > 0] if _tags else [] status = self.options.get("status") if status: @@ -71,7 +75,7 @@ def collect_filter_attributes(self) -> FilterAttributesType: types = [typ.strip() for typ in re.split(";|,", types)] # Add the need and all needed information - collected_filter_options = { + collected_filter_options: FilterAttributesType = { "status": status, "tags": tags, "types": types, @@ -85,8 +89,8 @@ def collect_filter_attributes(self) -> FilterAttributesType: def process_filters( - app: Sphinx, all_needs: List[NeedsInfoType], filter_data: NeedsFilteredBaseType, include_external: bool = True -) -> List[NeedsInfoType]: + app: Sphinx, all_needs: Iterable[NeedsInfoType], filter_data: NeedsFilteredBaseType, include_external: bool = True +) -> List[NeedsPartsInfoType]: """ Filters all needs with given configuration. Used by needlist, needtable and needflow. @@ -98,24 +102,25 @@ def process_filters( :return: list of needs, which passed the filters """ - + found_needs: List[NeedsPartsInfoType] sort_key = filter_data["sort_by"] if sort_key: try: - all_needs = sorted(all_needs, key=lambda node: node[sort_key] or "") + all_needs = sorted(all_needs, key=lambda node: node[sort_key] or "") # type: ignore[literal-required] except KeyError as e: log.warning(f"Sorting parameter {sort_key} not valid: Error: {e} [needs]", type="needs") # check if include external needs - checked_all_needs = [] + checked_all_needs: Iterable[NeedsInfoType] if not include_external: + checked_all_needs = [] for need in all_needs: if not need["is_external"]: checked_all_needs.append(need) else: checked_all_needs = all_needs - found_needs_by_options = [] + found_needs_by_options: List[NeedsPartsInfoType] = [] # Add all need_parts of given needs to the search list all_needs_incl_parts = prepare_need_list(checked_all_needs) @@ -160,13 +165,10 @@ def process_filters( found_needs = filter_needs(app, all_needs_incl_parts, filter_data["filter"]) else: # Provides only a copy of needs to avoid data manipulations. - try: - context = { - "needs": copy.deepcopy(all_needs_incl_parts), - "results": [], - } - except Exception as e: - raise e + context = { + "needs": copy.deepcopy(all_needs_incl_parts), + "results": [], + } if filter_code: # code from content exec(filter_code, context) @@ -186,7 +188,7 @@ def process_filters( return [] # The filter results may be dirty, as it may continue manipulated needs. - found_dirty_needs = context["results"] + found_dirty_needs: List[NeedsPartsInfoType] = context["results"] # type: ignore found_needs = [] # Check if config allow unsafe filters @@ -218,38 +220,43 @@ def process_filters( return found_needs -def prepare_need_list(need_list: List[NeedsInfoType]) -> List[NeedsInfoType]: +def prepare_need_list(need_list: Iterable[NeedsInfoType]) -> List[NeedsPartsInfoType]: # all_needs_incl_parts = need_list.copy() + all_needs_incl_parts: List[NeedsPartsInfoType] try: - all_needs_incl_parts = need_list[:] + all_needs_incl_parts = need_list[:] # type: ignore except TypeError: try: - all_needs_incl_parts = need_list.copy() + all_needs_incl_parts = need_list.copy() # type: ignore except AttributeError: - all_needs_incl_parts = list(need_list)[:] + all_needs_incl_parts = list(need_list)[:] # type: ignore for need in need_list: for part in need["parts"].values(): - filter_part = {**need, **part} # noqa: SIM904 - filter_part["id_parent"] = need["id"] - filter_part["id_complete"] = ".".join([need["id"], filter_part["id"]]) + id_complete = ".".join([need["id"], part["id"]]) + filter_part: NeedsPartsInfoType = {**need, **part, **{"id_parent": need["id"], "id_complete": id_complete}} all_needs_incl_parts.append(filter_part) # Be sure extra attributes, which makes only sense for need_parts, are also available on # need level so that no KeyError gets raised, if search/filter get executed on needs with a need-part argument. if "id_parent" not in need: - need["id_parent"] = need["id"] + need["id_parent"] = need["id"] # type: ignore[typeddict-unknown-key] if "id_complete" not in need: - need["id_complete"] = need["id"] + need["id_complete"] = need["id"] # type: ignore[typeddict-unknown-key] return all_needs_incl_parts -def intersection_of_need_results(list_a, list_b) -> List[Dict[str, Any]]: +T = TypeVar("T") + + +def intersection_of_need_results(list_a: List[T], list_b: List[T]) -> List[T]: return [a for a in list_a if a in list_b] @measure_time("filtering") -def filter_needs(app: Sphinx, needs: List[NeedsInfoType], filter_string: str = "", current_need=None): +def filter_needs( + app: Sphinx, needs: List[NeedsInfoType], filter_string: str = "", current_need: Optional[NeedsInfoType] = None +) -> List[NeedsInfoType]: """ Filters given needs based on a given filter string. Returns all needs, which pass the given filter. @@ -291,7 +298,7 @@ def filter_single_need( need: NeedsInfoType, filter_string: str = "", needs: Optional[List[NeedsInfoType]] = None, - current_need=None, + current_need: Optional[NeedsInfoType] = None, filter_compiled: Optional[CodeType] = None, ) -> bool: """ @@ -305,7 +312,7 @@ def filter_single_need( :param needs: list of all needs :return: True, if need passes the filter_string, else False """ - filter_context = need.copy() + filter_context: Dict[str, Any] = need.copy() # type: ignore if needs: filter_context["needs"] = needs if current_need: From 93d23763c272dad83c4483bcb826aac2c70f12f5 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 19:09:06 +0200 Subject: [PATCH 16/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.services.github`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/services/github.py | 21 ++++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9bcf7c01..96082dee8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,6 @@ module = [ 'sphinx_needs.functions.functions', 'sphinx_needs.layout', 'sphinx_needs.roles.need_ref', - 'sphinx_needs.services.github', 'sphinx_needs.utils', ] ignore_errors = true diff --git a/sphinx_needs/services/github.py b/sphinx_needs/services/github.py index 1594cad8d..0a8b6efdd 100644 --- a/sphinx_needs/services/github.py +++ b/sphinx_needs/services/github.py @@ -2,6 +2,7 @@ import textwrap import time from contextlib import suppress +from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse import requests @@ -24,7 +25,7 @@ class GithubService(BaseService): options = CONFIG_OPTIONS + EXTRA_DATA_OPTIONS + EXTRA_LINK_OPTIONS + EXTRA_IMAGE_OPTIONS - def __init__(self, app: Sphinx, name: str, config, **kwargs) -> None: + def __init__(self, app: Sphinx, name: str, config: Dict[str, Any], **kwargs: Any) -> None: self.app = app self.name = name self.config = config @@ -73,7 +74,7 @@ def __init__(self, app: Sphinx, name: str, config, **kwargs) -> None: super().__init__() - def _send(self, query, options, specific: bool = False): + def _send(self, query: str, options: Dict[str, Any], specific: bool = False) -> Dict[str, Any]: headers = {} if self.gh_type == "commit": headers["Accept"] = "application/vnd.github.cloak-preview+json" @@ -104,8 +105,10 @@ def _send(self, query, options, specific: bool = False): self.log.info(f"Service {self.name} requesting data for query: {query}") + auth: Optional[Tuple[str, str]] if self.username: - auth = (self.username, self.token) + # TODO token can be None + auth = (self.username, self.token) # type: ignore else: auth = None @@ -141,9 +144,9 @@ def _send(self, query, options, specific: bool = False): if specific: return {"items": [resp.json()]} - return resp.json() + return resp.json() # type: ignore - def request(self, options=None): + def request(self, options: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: if options is None: options = {} self.log.debug(f"Requesting data for service {self.name}") @@ -177,7 +180,7 @@ def request(self, options=None): return data - def prepare_issue_data(self, items, options): + def prepare_issue_data(self, items: List[Dict[str, Any]], options: Dict[str, Any]) -> List[Dict[str, Any]]: data = [] for item in items: # ensure that "None" can not reach .splitlines() @@ -187,7 +190,7 @@ def prepare_issue_data(self, items, options): # wraps content lines, if they are too long. Respects already existing newlines. content_lines = [ "\n ".join(textwrap.wrap(line, 60, break_long_words=True, replace_whitespace=False)) - for line in item["body"].splitlines() + for line in item["body"].splitlines() # type: ignore if line.strip() ] @@ -237,7 +240,7 @@ def prepare_issue_data(self, items, options): return data - def prepare_commit_data(self, items, options): + def prepare_commit_data(self, items: List[Dict[str, Any]], options: Dict[str, Any]) -> List[Dict[str, Any]]: data = [] for item in items: @@ -311,7 +314,7 @@ def _get_avatar(self, avatar_url: str) -> str: return avatar_file_path - def _add_given_options(self, options, element_data) -> None: + def _add_given_options(self, options: Dict[str, Any], element_data: Dict[str, Any]) -> None: """ Add data from options, which was defined by user but is not set by this service From d5818a4ec9d997fb5e35e58cc771c335b72a1539 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 21:48:16 +0200 Subject: [PATCH 17/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.utils`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/directives/needgantt.py | 26 ++++++++++++++++++++++---- sphinx_needs/directives/utils.py | 24 ++++-------------------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 96082dee8..8c2b9280c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ module = [ 'sphinx_needs.directives.needflow', 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needuml', - 'sphinx_needs.directives.utils', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', 'sphinx_needs.roles.need_ref', diff --git a/sphinx_needs/directives/needgantt.py b/sphinx_needs/directives/needgantt.py index 29f6fda4c..05a66862b 100644 --- a/sphinx_needs/directives/needgantt.py +++ b/sphinx_needs/directives/needgantt.py @@ -1,4 +1,5 @@ import os +import re from datetime import datetime from typing import List, Sequence @@ -19,7 +20,7 @@ get_filter_para, no_plantuml, ) -from sphinx_needs.directives.utils import get_link_type_option +from sphinx_needs.directives.utils import SphinxNeedsLinkTypeException from sphinx_needs.filter_common import FilterBase, filter_single_need, process_filters from sphinx_needs.logging import get_logger from sphinx_needs.utils import MONTH_NAMES, add_doc @@ -60,9 +61,9 @@ def run(self) -> Sequence[nodes.Node]: _id, targetid, targetnode = self.create_target("needgantt") - starts_with_links = get_link_type_option("starts_with_links", env, self, "") - starts_after_links = get_link_type_option("starts_after_links", env, self, "links") - ends_with_links = get_link_type_option("ends_with_links", env, self) + starts_with_links = self.get_link_type_option("starts_with_links") + starts_after_links = self.get_link_type_option("starts_after_links", "links") + ends_with_links = self.get_link_type_option("ends_with_links") milestone_filter = self.options.get("milestone_filter") start_date = self.options.get("start_date") @@ -114,6 +115,23 @@ def run(self) -> Sequence[nodes.Node]: return [targetnode] + [Needgantt("")] + def get_link_type_option(self, name: str, default: str = "") -> List[str]: + link_types = [x.strip() for x in re.split(";|,", self.options.get(name, default))] + conf_link_types = NeedsSphinxConfig(self.env.config).extra_links + conf_link_types_name = [x["option"] for x in conf_link_types] + + final_link_types = [] + for link_type in link_types: + if link_type is None or link_type == "": + continue + if link_type not in conf_link_types_name: + raise SphinxNeedsLinkTypeException( + link_type + "does not exist in configuration option needs_extra_links" + ) + + final_link_types.append(link_type) + return final_link_types + def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, found_nodes: List[nodes.Element]) -> None: # Replace all needgantt nodes with a list of the collected needs. diff --git a/sphinx_needs/directives/utils.py b/sphinx_needs/directives/utils.py index 00a777ee4..4c33bc022 100644 --- a/sphinx_needs/directives/utils.py +++ b/sphinx_needs/directives/utils.py @@ -5,7 +5,7 @@ from sphinx.environment import BuildEnvironment from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.data import NeedsFilteredBaseType, SphinxNeedsData from sphinx_needs.defaults import TITLE_REGEX @@ -17,7 +17,7 @@ def no_needs_found_paragraph() -> nodes.paragraph: return para -def used_filter_paragraph(current_needfilter) -> nodes.paragraph: +def used_filter_paragraph(current_needfilter: NeedsFilteredBaseType) -> nodes.paragraph: para = nodes.paragraph() filter_text = "Used filter:" filter_text += ( @@ -39,22 +39,6 @@ def used_filter_paragraph(current_needfilter) -> nodes.paragraph: return para -def get_link_type_option(name: str, env: BuildEnvironment, node, default: str = "") -> List[str]: - link_types = [x.strip() for x in re.split(";|,", node.options.get(name, default))] - conf_link_types = NeedsSphinxConfig(env.config).extra_links - conf_link_types_name = [x["option"] for x in conf_link_types] - - final_link_types = [] - for link_type in link_types: - if link_type is None or link_type == "": - continue - if link_type not in conf_link_types_name: - raise SphinxNeedsLinkTypeException(link_type + "does not exist in configuration option needs_extra_links") - - final_link_types.append(link_type) - return final_link_types - - def get_title(option_string: str) -> Tuple[str, str]: """ Returns a tuple of uppercase option and calculated title of given option string. @@ -74,7 +58,7 @@ def get_title(option_string: str) -> Tuple[str, str]: return option_name.upper(), title -def get_option_list(options, name: str) -> List[str]: +def get_option_list(options: Dict[str, Any], name: str) -> List[str]: """ Gets and creates a list of a given directive option value in a safe way :param options: List of options @@ -97,7 +81,7 @@ def analyse_needs_metrics(env: BuildEnvironment) -> Dict[str, Any]: :return: Dictionary consisting of needs metrics. """ needs = SphinxNeedsData(env).get_or_create_needs() - metric_data = {"needs_amount": len(needs)} + metric_data: Dict[str, Any] = {"needs_amount": len(needs)} needs_types = {i["directive"]: 0 for i in NeedsSphinxConfig(env.config).types} for i in needs.values(): From e3261315667421333f3e79c8a460eeccc091d9c9 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 21:54:46 +0200 Subject: [PATCH 18/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needfilter`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/directives/needfilter.py | 18 ++++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c2b9280c..37a29328a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,6 @@ module = [ 'sphinx_needs.directives.need', 'sphinx_needs.directives.needbar', 'sphinx_needs.directives.needextend', - 'sphinx_needs.directives.needfilter', 'sphinx_needs.directives.needflow', 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needuml', diff --git a/sphinx_needs/directives/needfilter.py b/sphinx_needs/directives/needfilter.py index 09d52c586..ef196bb73 100644 --- a/sphinx_needs/directives/needfilter.py +++ b/sphinx_needs/directives/needfilter.py @@ -5,17 +5,11 @@ from docutils import nodes from docutils.parsers.rst import directives from jinja2 import Template +from sphinx.application import Sphinx +from sphinx.errors import NoUri from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import SphinxNeedsData - -try: - from sphinx.errors import NoUri # Sphinx 3.0 -except ImportError: - from sphinx.environment import NoUri # Sphinx < 3.0 - -from sphinx.application import Sphinx - from sphinx_needs.diagrams_common import create_legend from sphinx_needs.filter_common import FilterBase, process_filters from sphinx_needs.utils import add_doc, row_col_maker, unwrap @@ -35,7 +29,7 @@ class NeedfilterDirective(FilterBase): @staticmethod def layout(argument: str) -> str: - return directives.choice(argument, ("list", "table", "diagram")) + return directives.choice(argument, ("list", "table", "diagram")) # type: ignore option_spec = { "show_status": directives.flag, @@ -83,6 +77,7 @@ def process_needfilters( builder = unwrap(app.builder) env = unwrap(builder.env) needs_config = NeedsSphinxConfig(env.config) + all_needs = SphinxNeedsData(env).get_or_create_needs() # NEEDFILTER # for node in doctree.findall(Needfilter): @@ -100,8 +95,8 @@ def process_needfilters( id = node.attributes["ids"][0] current_needfilter = SphinxNeedsData(env)._get_or_create_filters()[id] - all_needs = SphinxNeedsData(env).get_or_create_needs() + content: List[nodes.Element] if current_needfilter["layout"] == "list": content = [] @@ -156,8 +151,7 @@ def process_needfilters( tgroup += tbody content += tgroup - all_needs = list(all_needs.values()) - found_needs = process_filters(app, all_needs, current_needfilter) + found_needs = process_filters(app, all_needs.values(), current_needfilter) line_block = nodes.line_block() for need_info in found_needs: From 2e65cdd0f1808bd911178f08c57edd0afba0573c Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Aug 2023 22:42:05 +0200 Subject: [PATCH 19/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needflow`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/data.py | 14 ++++++-- sphinx_needs/diagrams_common.py | 7 ++-- sphinx_needs/directives/needflow.py | 55 ++++++++++++++++++----------- 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37a29328a..83231e94a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,6 @@ module = [ 'sphinx_needs.directives.need', 'sphinx_needs.directives.needbar', 'sphinx_needs.directives.needextend', - 'sphinx_needs.directives.needflow', 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index 5c059dede..3b5345419 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -149,8 +149,9 @@ class NeedsInfoType(NeedsBaseDataType): """List of need IDs, which are referenced by this need.""" links_back: list[str] """List of need IDs, which are referencing this need.""" - # TODO there is a lot more dynamically added link information; - # for each item in needs_extra_links config, + # TODO there is more dynamically added link information; + # for each item in needs_extra_links config + # (and in prepare_env 'links' and 'parent_needs' are added if not present), # you end up with a key named by the "option" field, # and then another key named by the "option" field + "_back" # these all have value type `list[str]` @@ -172,8 +173,15 @@ class NeedsInfoType(NeedsBaseDataType): signature: str | Text """Derived from a docutils desc_name node""" parent_needs: list[str] + """List of parents of the this need (by id), + i.e. if this need is nested in another + """ + parent_needs_back: list[str] + """List of children of this need (by id), + i.e. if needs are nested within this one + """ parent_need: str - """Simply the first parent""" + """Simply the first parent id""" # default extra options # TODO these all default to "" which I don't think is good diff --git a/sphinx_needs/diagrams_common.py b/sphinx_needs/diagrams_common.py index c34f22af6..243f17533 100644 --- a/sphinx_needs/diagrams_common.py +++ b/sphinx_needs/diagrams_common.py @@ -15,7 +15,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import NeedsFilteredBaseType +from sphinx_needs.data import NeedsFilteredBaseType, NeedsPartsInfoType from sphinx_needs.errors import NoUri from sphinx_needs.logging import get_logger from sphinx_needs.utils import get_scale, split_link_types, unwrap @@ -156,7 +156,7 @@ def get_debug_container(puml_node: nodes.Element) -> nodes.container: return debug_container -def calculate_link(app: Sphinx, need_info: Dict[str, Any], _fromdocname: str) -> str: +def calculate_link(app: Sphinx, need_info: NeedsPartsInfoType, _fromdocname: str) -> str: """ Link calculation All links we can get from docutils functions will be relative. @@ -172,7 +172,8 @@ def calculate_link(app: Sphinx, need_info: Dict[str, Any], _fromdocname: str) -> builder = unwrap(app.builder) try: if need_info["is_external"]: - link: str = need_info["external_url"] + assert need_info["external_url"] is not None, "external_url must be set for external needs" + link = need_info["external_url"] # check if need_info["external_url"] is relative path parsed_url = urlparse(need_info["external_url"]) if not parsed_url.scheme and not os.path.isabs(need_info["external_url"]): diff --git a/sphinx_needs/directives/needflow.py b/sphinx_needs/directives/needflow.py index 342bd3abd..19315e33c 100644 --- a/sphinx_needs/directives/needflow.py +++ b/sphinx_needs/directives/needflow.py @@ -1,6 +1,6 @@ import html import os -from typing import List, Sequence +from typing import Dict, Iterable, List, Sequence from docutils import nodes from docutils.parsers.rst import directives @@ -11,7 +11,12 @@ ) from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.data import ( + NeedsFlowType, + NeedsInfoType, + NeedsPartsInfoType, + SphinxNeedsData, +) from sphinx_needs.debug import measure_time from sphinx_needs.diagrams_common import calculate_link, create_legend from sphinx_needs.filter_common import FilterBase, filter_single_need, process_filters @@ -21,7 +26,7 @@ logger = get_logger(__name__) -NEEDFLOW_TEMPLATES = {} +NEEDFLOW_TEMPLATES: Dict[str, Template] = {} class Needflow(nodes.General, nodes.Element): @@ -106,7 +111,11 @@ def make_entity_name(name: str) -> str: def get_need_node_rep_for_plantuml( - app: Sphinx, fromdocname: str, current_needflow: dict, all_needs: list, need_info: dict + app: Sphinx, + fromdocname: str, + current_needflow: NeedsFlowType, + all_needs: Iterable[NeedsInfoType], + need_info: NeedsPartsInfoType, ) -> str: """Calculate need node representation for plantuml.""" needs_config = NeedsSphinxConfig(app.config) @@ -144,11 +153,11 @@ def get_need_node_rep_for_plantuml( def walk_curr_need_tree( app: Sphinx, fromdocname: str, - current_needflow: dict, - all_needs: list, - found_needs: list, - need: dict, -): + current_needflow: NeedsFlowType, + all_needs: Iterable[NeedsInfoType], + found_needs: List[NeedsPartsInfoType], + need: NeedsPartsInfoType, +) -> str: """ Walk through each need to find all its child needs and need parts recursively and wrap them together in nested structure. """ @@ -213,7 +222,7 @@ def walk_curr_need_tree( return curr_need_tree -def get_root_needs(found_needs: list) -> list: +def get_root_needs(found_needs: List[NeedsPartsInfoType]) -> List[NeedsPartsInfoType]: return_list = [] for current_need in found_needs: if current_need["is_need"]: @@ -231,7 +240,13 @@ def get_root_needs(found_needs: list) -> list: return return_list -def cal_needs_node(app: Sphinx, fromdocname: str, current_needflow: dict, all_needs: list, found_needs: list) -> str: +def cal_needs_node( + app: Sphinx, + fromdocname: str, + current_needflow: NeedsFlowType, + all_needs: Iterable[NeedsInfoType], + found_needs: List[NeedsPartsInfoType], +) -> str: """Calculate and get needs node representaion for plantuml including all child needs and need parts.""" top_needs = get_root_needs(found_needs) curr_need_tree = "" @@ -259,8 +274,10 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou env = unwrap(app.env) needs_config = NeedsSphinxConfig(app.config) env_data = SphinxNeedsData(env) + all_needs = env_data.get_or_create_needs() link_types = needs_config.extra_links + link_type_names = [link["option"].upper() for link in link_types] allowed_link_types_options = [link.upper() for link in needs_config.flow_link_types] # NEEDFLOW @@ -279,14 +296,13 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou id = node.attributes["ids"][0] current_needflow = env_data.get_or_create_flows()[id] - all_needs = env_data.get_or_create_needs() option_link_types = [link.upper() for link in current_needflow["link_types"]] for lt in option_link_types: - if lt not in [link["option"].upper() for link in link_types]: + if lt not in link_type_names: logger.warning( "Unknown link type {link_type} in needflow {flow}. Allowed values: {link_types} [needs]".format( - link_type=lt, flow=current_needflow["target_id"], link_types=",".join(link_types) + link_type=lt, flow=current_needflow["target_id"], link_types=",".join(link_type_names) ), type="needs", ) @@ -305,8 +321,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou node.replace_self(content) continue - all_needs = list(all_needs.values()) - found_needs = process_filters(app, all_needs, current_needflow) + found_needs = process_filters(app, all_needs.values(), current_needflow) if found_needs: plantuml_block_text = ".. plantuml::\n" "\n" " @startuml" " @enduml" @@ -344,7 +359,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou if link_type["option"] == "parent_needs": continue - for link in need_info[link_type["option"]]: + for link in need_info[link_type["option"]]: # type: ignore[literal-required] # If source or target of link is a need_part, a specific style is needed if "." in link or "." in need_info["id_complete"]: final_link = link @@ -396,7 +411,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou ) # calculate needs node representation for plantuml - puml_node["uml"] += cal_needs_node(app, fromdocname, current_needflow, all_needs, found_needs) + puml_node["uml"] += cal_needs_node(app, fromdocname, current_needflow, all_needs.values(), found_needs) puml_node["uml"] += "\n' Connection definition \n\n" puml_node["uml"] += puml_connections @@ -489,10 +504,10 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou node.replace_self(content) -def get_template(template_name): +def get_template(template_name: str) -> Template: """Checks if a template got already rendered, if it's the case, return it""" - if template_name not in NEEDFLOW_TEMPLATES.keys(): + if template_name not in NEEDFLOW_TEMPLATES: NEEDFLOW_TEMPLATES[template_name] = Template(template_name) return NEEDFLOW_TEMPLATES[template_name] From a723f9d1983a0326568f7201d8762e5e6dd31014 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 00:08:01 +0200 Subject: [PATCH 20/29] =?UTF-8?q?=F0=9F=94=A7=20Fix=20typing=20of=20`debug?= =?UTF-8?q?.measure=5Ftime`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sphinx_needs/builder.py | 7 ++-- sphinx_needs/data.py | 4 +-- sphinx_needs/debug.py | 51 ++++++++++++++------------- sphinx_needs/directives/needimport.py | 8 ++--- sphinx_needs/filter_common.py | 49 ++++++++++++++----------- sphinx_needs/functions/functions.py | 6 ++-- sphinx_needs/need_constraints.py | 9 +++-- 7 files changed, 70 insertions(+), 64 deletions(-) diff --git a/sphinx_needs/builder.py b/sphinx_needs/builder.py index 3be968ab4..4b1277837 100644 --- a/sphinx_needs/builder.py +++ b/sphinx_needs/builder.py @@ -1,5 +1,5 @@ import os -from typing import Iterable, Optional, Set +from typing import Iterable, List, Optional, Set from docutils import nodes from sphinx import version_info @@ -7,7 +7,7 @@ from sphinx.builders import Builder from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.data import NeedsInfoType, SphinxNeedsData from sphinx_needs.logging import get_logger from sphinx_needs.needsfile import NeedsList from sphinx_needs.utils import unwrap @@ -27,7 +27,6 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None: def finish(self) -> None: env = unwrap(self.env) data = SphinxNeedsData(env) - needs = data.get_or_create_needs().values() # We need a list of needs for later filter checks filters = data.get_or_create_filters() version = getattr(env.config, "version", "unset") needs_list = NeedsList(env.config, self.outdir, self.srcdir) @@ -50,7 +49,7 @@ def finish(self) -> None: from sphinx_needs.filter_common import filter_needs filter_string = needs_config.builder_filter - filtered_needs = filter_needs(self.app, needs, filter_string) + filtered_needs: List[NeedsInfoType] = filter_needs(self.app, data.get_or_create_needs().values(), filter_string) for need in filtered_needs: needs_list.add_need(version, need) diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index 3b5345419..370825018 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -3,7 +3,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any try: from typing import Literal, TypedDict @@ -161,7 +161,7 @@ class NeedsInfoType(NeedsBaseDataType): # set in process_need_nodes (-> process_constraints) transform constraints: list[str] constraints_passed: None | bool - constraints_results: dict[str, dict[str, bool]] + constraints_results: dict[str, dict[str, Any]] # additional source information doctype: str diff --git a/sphinx_needs/debug.py b/sphinx_needs/debug.py index 7243c6c57..b340510bf 100644 --- a/sphinx_needs/debug.py +++ b/sphinx_needs/debug.py @@ -2,6 +2,7 @@ Contains debug features to track down runtime and other problems with Sphinx-Needs """ +from __future__ import annotations import inspect import json @@ -10,22 +11,22 @@ from functools import wraps from pathlib import Path from timeit import default_timer as timer # Used for timing measurements -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, TypeVar from jinja2 import Environment, PackageLoader, select_autoescape from sphinx.application import Sphinx -TIME_MEASUREMENTS: Dict[str, Any] = {} # Stores the timing results +TIME_MEASUREMENTS: dict[str, Any] = {} # Stores the timing results EXECUTE_TIME_MEASUREMENTS = False # Will be used to de/activate measurements. Set during a Sphinx Event START_TIME = 0.0 +T = TypeVar("T", bound=Callable[..., Any]) -def measure_time( - category: "str | None" = None, source: str = "internal", name: "str | None" = None, func: "object | None" = None -) -> Callable[..., Callable[..., Any]]: + +def measure_time(category: str | None = None, source: str = "internal", name: str | None = None) -> Callable[[T], T]: """ - Measures the needed execution time of a specific function. + Decorator for measuring the needed execution time of a specific function. It measures: @@ -46,23 +47,15 @@ def measure_time( def my_cool_function(a, b,c ): # does something - Usage as function:: - - from sphinx_needs.utils import measure_time - - # Old call: my_cool_function(a,b,c) - new_func = measure_time('my_category', func=my_cool_function) - new_func(a,b,c) - :param category: Name of a category, which helps to cluster the measured functions. :param source: Should be "internal" or "user". Used to easily structure function written by user. :param name: Name to use for the measured. If not given, the function name is used. :param func: Can contain a func, which shall get decorated. Not used if ``measure_time`` is used as decorator. """ - def inner(func: Any) -> Callable[..., Any]: + def inner(func: T) -> T: @wraps(func) - def wrapper(*args: List[object], **kwargs: Dict[object, object]) -> Any: + def wrapper(*args: list[object], **kwargs: dict[object, object]) -> Any: """ Wrapper function around a given/decorated function, which cares about measurement and storing the result @@ -121,15 +114,25 @@ def wrapper(*args: List[object], **kwargs: Dict[object, object]) -> Any: runtime_dict["avg"] = runtime_dict["overall"] / runtime_dict["amount"] return result - return wrapper - - # if `measure_time` is used as function and not as decorator, execute the `inner()` with given func directly - if func is not None: - return inner(func) + return wrapper # type: ignore return inner +def measure_time_func(func: T, category: str | None = None, source: str = "internal", name: str | None = None) -> T: + """Wrapper for measuring the needed execution time of a specific function. + + Usage as function:: + + from sphinx_needs.utils import measure_time + + # Old call: my_cool_function(a,b,c) + new_func = measure_time_func('my_category', func=my_cool_function) + new_func(a,b,c) + """ + return measure_time(category, source, name)(func) + + def print_timing_results() -> None: for value in TIME_MEASUREMENTS.values(): print(value["name"]) @@ -140,7 +143,7 @@ def print_timing_results() -> None: print(f' min: {value["min"]:2f} \n') -def store_timing_results_json(outdir: str, build_data: Dict[str, Any]) -> None: +def store_timing_results_json(outdir: str, build_data: dict[str, Any]) -> None: json_result_path = os.path.join(outdir, "debug_measurement.json") data = {"build": build_data, "measurements": TIME_MEASUREMENTS} @@ -150,7 +153,7 @@ def store_timing_results_json(outdir: str, build_data: Dict[str, Any]) -> None: print(f"Timing measurement results (JSON) stored under {json_result_path}") -def store_timing_results_html(outdir: str, build_data: Dict[str, Any]) -> None: +def store_timing_results_html(outdir: str, build_data: dict[str, Any]) -> None: jinja_env = Environment(loader=PackageLoader("sphinx_needs"), autoescape=select_autoescape()) template = jinja_env.get_template("time_measurements.html") out_file = Path(outdir) / "debug_measurement.html" @@ -159,7 +162,7 @@ def store_timing_results_html(outdir: str, build_data: Dict[str, Any]) -> None: print(f"Timing measurement report (HTML) stored under {out_file}") -def process_timing(app: Sphinx, _exception: Optional[Exception]) -> None: +def process_timing(app: Sphinx, _exception: Exception | None) -> None: if EXECUTE_TIME_MEASUREMENTS: build_data = { "project": app.config["project"], diff --git a/sphinx_needs/directives/needimport.py b/sphinx_needs/directives/needimport.py index 7e682276f..d7bf49f62 100644 --- a/sphinx_needs/directives/needimport.py +++ b/sphinx_needs/directives/needimport.py @@ -123,6 +123,7 @@ def run(self) -> Sequence[nodes.Node]: if version not in needs_import_list["versions"].keys(): raise VersionNotFound(f"Version {version} not found in needs import file {correct_need_import_path}") + # TODO type this (it uncovers lots of bugs) needs_list = needs_import_list["versions"][version]["needs"] # Filter imported needs @@ -131,8 +132,7 @@ def run(self) -> Sequence[nodes.Node]: if filter_string is None: needs_list_filtered[key] = need else: - # filter_context = {key: value for key, value in need.items()} - filter_context = dict(need) + filter_context = need.copy() # Support both ways of addressing the description, as "description" is used in json file, but # "content" is the sphinx internal name for this kind of information @@ -154,10 +154,8 @@ def run(self) -> Sequence[nodes.Node]: # If we need to set an id prefix, we also need to manipulate all used ids in the imported data. extra_links = NeedsSphinxConfig(self.config).extra_links if id_prefix: - needs_ids = needs_list.keys() - for need in needs_list.values(): - for id in needs_ids: + for id in needs_list: # Manipulate links in all link types for extra_link in extra_links: if extra_link["option"] in need and id in need[extra_link["option"]]: diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index 0e8d22295..446653e6e 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -2,11 +2,12 @@ filter_base is used to provide common filter functionality for directives like needtable, needlist and needflow. """ +from __future__ import annotations import copy import re from types import CodeType -from typing import Any, Dict, Iterable, List, Optional, TypeVar +from typing import Any, Iterable, TypeVar from docutils.parsers.rst import directives from sphinx.application import Sphinx @@ -20,7 +21,7 @@ NeedsPartsInfoType, SphinxNeedsData, ) -from sphinx_needs.debug import measure_time +from sphinx_needs.debug import measure_time, measure_time_func from sphinx_needs.utils import check_and_get_external_filter_func from sphinx_needs.utils import logger as log @@ -32,12 +33,12 @@ class FilterAttributesType(TypedDict): - status: List[str] - tags: List[str] - types: List[str] + status: list[str] + tags: list[str] + types: list[str] filter: str sort_by: str - filter_code: List[str] + filter_code: list[str] filter_func: str export_id: str @@ -90,7 +91,7 @@ def collect_filter_attributes(self) -> FilterAttributesType: def process_filters( app: Sphinx, all_needs: Iterable[NeedsInfoType], filter_data: NeedsFilteredBaseType, include_external: bool = True -) -> List[NeedsPartsInfoType]: +) -> list[NeedsPartsInfoType]: """ Filters all needs with given configuration. Used by needlist, needtable and needflow. @@ -102,7 +103,7 @@ def process_filters( :return: list of needs, which passed the filters """ - found_needs: List[NeedsPartsInfoType] + found_needs: list[NeedsPartsInfoType] sort_key = filter_data["sort_by"] if sort_key: try: @@ -120,7 +121,7 @@ def process_filters( else: checked_all_needs = all_needs - found_needs_by_options: List[NeedsPartsInfoType] = [] + found_needs_by_options: list[NeedsPartsInfoType] = [] # Add all need_parts of given needs to the search list all_needs_incl_parts = prepare_need_list(checked_all_needs) @@ -181,14 +182,14 @@ def process_filters( context[f"arg{index+1}"] = arg # Decorate function to allow time measurments - filter_func = measure_time(category="filter_func", source="user", func=filter_func) + filter_func = measure_time_func(filter_func, category="filter_func", source="user") filter_func(**context) else: log.warning("Something went wrong running filter [needs]", type="needs") return [] # The filter results may be dirty, as it may continue manipulated needs. - found_dirty_needs: List[NeedsPartsInfoType] = context["results"] # type: ignore + found_dirty_needs: list[NeedsPartsInfoType] = context["results"] # type: ignore found_needs = [] # Check if config allow unsafe filters @@ -220,9 +221,9 @@ def process_filters( return found_needs -def prepare_need_list(need_list: Iterable[NeedsInfoType]) -> List[NeedsPartsInfoType]: +def prepare_need_list(need_list: Iterable[NeedsInfoType]) -> list[NeedsPartsInfoType]: # all_needs_incl_parts = need_list.copy() - all_needs_incl_parts: List[NeedsPartsInfoType] + all_needs_incl_parts: list[NeedsPartsInfoType] try: all_needs_incl_parts = need_list[:] # type: ignore except TypeError: @@ -249,14 +250,20 @@ def prepare_need_list(need_list: Iterable[NeedsInfoType]) -> List[NeedsPartsInfo T = TypeVar("T") -def intersection_of_need_results(list_a: List[T], list_b: List[T]) -> List[T]: +def intersection_of_need_results(list_a: list[T], list_b: list[T]) -> list[T]: return [a for a in list_a if a in list_b] +V = TypeVar("V", bound=NeedsInfoType) + + @measure_time("filtering") def filter_needs( - app: Sphinx, needs: List[NeedsInfoType], filter_string: str = "", current_need: Optional[NeedsInfoType] = None -) -> List[NeedsInfoType]: + app: Sphinx, + needs: Iterable[V], + filter_string: None | str = "", + current_need: NeedsInfoType | None = None, +) -> list[V]: """ Filters given needs based on a given filter string. Returns all needs, which pass the given filter. @@ -270,7 +277,7 @@ def filter_needs( """ if not filter_string: - return needs + return list(needs) found_needs = [] @@ -297,9 +304,9 @@ def filter_single_need( app: Sphinx, need: NeedsInfoType, filter_string: str = "", - needs: Optional[List[NeedsInfoType]] = None, - current_need: Optional[NeedsInfoType] = None, - filter_compiled: Optional[CodeType] = None, + needs: Iterable[NeedsInfoType] | None = None, + current_need: NeedsInfoType | None = None, + filter_compiled: CodeType | None = None, ) -> bool: """ Checks if a single need/need_part passes a filter_string @@ -312,7 +319,7 @@ def filter_single_need( :param needs: list of all needs :return: True, if need passes the filter_string, else False """ - filter_context: Dict[str, Any] = need.copy() # type: ignore + filter_context: dict[str, Any] = need.copy() # type: ignore if needs: filter_context["needs"] = needs if current_need: diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index 9c15aa03d..cf355d3a1 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -17,7 +17,7 @@ from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import SphinxNeedsData -from sphinx_needs.debug import measure_time +from sphinx_needs.debug import measure_time_func from sphinx_needs.logging import get_logger from sphinx_needs.utils import NEEDS_FUNCTIONS, match_variants # noqa: F401 @@ -67,10 +67,10 @@ def execute_func(env: BuildEnvironment, need, func_string: str): global NEEDS_FUNCTIONS func_name, func_args, func_kwargs = _analyze_func_string(func_string, need) - if func_name not in NEEDS_FUNCTIONS.keys(): + if func_name not in NEEDS_FUNCTIONS: raise SphinxError("Unknown dynamic sphinx-needs function: {}. Found in need: {}".format(func_name, need["id"])) - func = measure_time(category="dyn_func", source="user", func=NEEDS_FUNCTIONS[func_name]["function"]) + func = measure_time_func(NEEDS_FUNCTIONS[func_name]["function"], category="dyn_func", source="user") func_return = func(env.app, need, SphinxNeedsData(env).get_or_create_needs(), *func_args, **func_kwargs) if not isinstance(func_return, (str, int, float, list, unicode)) and func_return: diff --git a/sphinx_needs/need_constraints.py b/sphinx_needs/need_constraints.py index 59c11c650..19f3b618a 100644 --- a/sphinx_needs/need_constraints.py +++ b/sphinx_needs/need_constraints.py @@ -1,16 +1,15 @@ -from typing import Any, Dict - from sphinx.application import Sphinx from sphinx_needs.api.exceptions import NeedsConstraintFailed, NeedsConstraintNotAllowed from sphinx_needs.config import NeedsSphinxConfig +from sphinx_needs.data import NeedsInfoType from sphinx_needs.filter_common import filter_single_need from sphinx_needs.logging import get_logger logger = get_logger(__name__) -def process_constraints(app: Sphinx, need: Dict[str, Any]) -> None: +def process_constraints(app: Sphinx, need: NeedsInfoType) -> None: """ Finally creates the need-node in the docurils node-tree. @@ -47,14 +46,14 @@ def process_constraints(app: Sphinx, need: Dict[str, Any]) -> None: if not constraint_passed: # prepare structure per name - if constraint not in need["constraints_results"].keys(): + if constraint not in need["constraints_results"]: need["constraints_results"][constraint] = {} # defines what to do if a constraint is not fulfilled. from conf.py constraint_failed_options = needs_config.constraint_failed_options # prepare structure for check_0, check_1 ... - if name not in need["constraints_results"][constraint].keys(): + if name not in need["constraints_results"][constraint]: need["constraints_results"][constraint][name] = {} need["constraints_results"][constraint][name] = False From fb8fda995149218db203c2cc587e010b31fc5e70 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 00:47:46 +0200 Subject: [PATCH 21/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.roles.need=5Frefs`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/roles/need_ref.py | 46 ++++++++++++++++------------------ 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 83231e94a..4ca8e1f4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ module = [ 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', - 'sphinx_needs.roles.need_ref', 'sphinx_needs.utils', ] ignore_errors = true diff --git a/sphinx_needs/roles/need_ref.py b/sphinx_needs/roles/need_ref.py index 32ff5ac86..d7f839ec5 100644 --- a/sphinx_needs/roles/need_ref.py +++ b/sphinx_needs/roles/need_ref.py @@ -1,26 +1,25 @@ import contextlib from collections.abc import Iterable -from typing import Dict, List +from typing import Dict, List, Union from docutils import nodes from sphinx.application import Sphinx from sphinx.util.nodes import make_refnode from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.data import NeedsInfoType, SphinxNeedsData from sphinx_needs.errors import NoUri from sphinx_needs.logging import get_logger -from sphinx_needs.nodes import Need from sphinx_needs.utils import check_and_calc_base_url_rel_path, unwrap log = get_logger(__name__) -class NeedRef(nodes.Inline, nodes.Element): +class NeedRef(nodes.Inline, nodes.Element): # type: ignore pass -def transform_need_to_dict(need: Need) -> Dict[str, str]: +def transform_need_to_dict(need: NeedsInfoType) -> Dict[str, str]: """ The function will transform a need in a dictionary of strings. Used to be given e.g. to a python format string. @@ -37,18 +36,16 @@ def transform_need_to_dict(need: Need) -> Dict[str, str]: """ dict_need = {} - for element in need: - if isinstance(need[element], str): + for element, value in need.items(): + if isinstance(value, str): # As string are iterable, we have to handle strings first. - dict_need[element] = need[element] - elif isinstance(need[element], list): - dict_need[element] = ";".join(need[element]) - elif isinstance(need[element], dict): - dict_need[element] = ";".join([str(i) for i in need[element].items()]) - elif isinstance(need[element], Iterable): - dict_need[element] = ";".join([str(i) for i in need[element]]) + dict_need[element] = value + elif isinstance(value, dict): + dict_need[element] = ";".join([str(i) for i in value.items()]) + elif isinstance(value, (Iterable, list, tuple)): + dict_need[element] = ";".join([str(i) for i in value]) else: - dict_need[element] = need[element] + dict_need[element] = str(value) return dict_need @@ -101,7 +98,7 @@ def process_need_ref(app: Sphinx, doctree: nodes.document, fromdocname: str, fou title = f"{title[: max_length - 3]}..." dict_need["title"] = title - ref_name = node_need_ref.children[0].children[0] + ref_name: Union[None, str, nodes.Text] = node_need_ref.children[0].children[0] # type: ignore[assignment] # Only use ref_name, if it differs from ref_id if str(ref_id_complete) == str(ref_name): ref_name = None @@ -112,13 +109,11 @@ def process_need_ref(app: Sphinx, doctree: nodes.document, fromdocname: str, fou try: link_text = ref_name.format(**dict_need) except KeyError as e: - link_text = '"option placeholder %s for need %s not found (Line %i of file %s)"' % ( - e, - node_need_ref["reftarget"], - node_need_ref.line, - node_need_ref.source, + log.warning( + f"option placeholder {e} for need {node_need_ref['reftarget']} not found [needs]", + type="needs", + location=node_need_ref, ) - log.warning(link_text + " [needs]", type="needs") else: if ref_name: # If ref_name differs from the need id, we treat the "ref_name content" as title. @@ -131,7 +126,7 @@ def process_need_ref(app: Sphinx, doctree: nodes.document, fromdocname: str, fou ) log.warning(link_text + " [needs]", type="needs") - node_need_ref[0].children[0] = nodes.Text(link_text) + node_need_ref[0].children[0] = nodes.Text(link_text) # type: ignore[index] with contextlib.suppress(NoUri): if not target_need.get("is_external", False): @@ -144,15 +139,16 @@ def process_need_ref(app: Sphinx, doctree: nodes.document, fromdocname: str, fou node_need_ref["reftarget"], ) else: + assert target_need["external_url"] is not None, "external_url must be set for external needs" new_node_ref = nodes.reference(target_need["id"], target_need["id"]) new_node_ref["refuri"] = check_and_calc_base_url_rel_path(target_need["external_url"], fromdocname) new_node_ref["classes"].append(target_need["external_css"]) else: log.warning( - "linked need %s not found (Line %i of file %s) [needs]" - % (node_need_ref["reftarget"], node_need_ref.line, node_need_ref.source), + f"linked need {node_need_ref['reftarget']} not found [needs]", type="needs", + location=node_need_ref, ) node_need_ref.replace_self(new_node_ref) From 9a2577dca934786c3651bc313dbaa26e88e04e33 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 00:57:13 +0200 Subject: [PATCH 22/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needpie`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 3 ++- sphinx_needs/directives/needpie.py | 2 +- sphinx_needs/filter_common.py | 2 +- sphinx_needs/utils.py | 6 ++---- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ca8e1f4c..7a92aa9df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,8 @@ namespace_packages = true [[tool.mypy.overrides]] module = [ + "matplotlib.*", + "numpy.*", "requests_file", "sphinx_data_viewer.*", "sphinxcontrib.plantuml.*", @@ -102,7 +104,6 @@ module = [ 'sphinx_needs.directives.need', 'sphinx_needs.directives.needbar', 'sphinx_needs.directives.needextend', - 'sphinx_needs.directives.needpie', 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', diff --git a/sphinx_needs/directives/needpie.py b/sphinx_needs/directives/needpie.py index a3b56dea0..1dcb2982a 100644 --- a/sphinx_needs/directives/needpie.py +++ b/sphinx_needs/directives/needpie.py @@ -154,7 +154,7 @@ def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, foun elif current_needpie["filter_func"] and not content: try: # check and get filter_func - filter_func, filter_args = check_and_get_external_filter_func(current_needpie) + filter_func, filter_args = check_and_get_external_filter_func(current_needpie.get("filter_func")) # execute filter_func code # Provides only a copy of needs to avoid data manipulations. context = { diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index 446653e6e..a00795c56 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -127,7 +127,7 @@ def process_filters( all_needs_incl_parts = prepare_need_list(checked_all_needs) # Check if external filter code is defined - filter_func, filter_args = check_and_get_external_filter_func(filter_data) + filter_func, filter_args = check_and_get_external_filter_func(filter_data.get("filter_func")) filter_code = None # Get filter_code from diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index b7d8a4db4..6da53e8e1 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -14,7 +14,7 @@ from sphinx.application import BuildEnvironment, Sphinx from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import NeedsFilteredBaseType, NeedsInfoType, SphinxNeedsData +from sphinx_needs.data import NeedsInfoType, SphinxNeedsData from sphinx_needs.defaults import NEEDS_PROFILING from sphinx_needs.logging import get_logger @@ -304,14 +304,12 @@ def check_and_calc_base_url_rel_path(external_url: str, fromdocname: str) -> str return ref_uri -def check_and_get_external_filter_func(filter_data: NeedsFilteredBaseType): +def check_and_get_external_filter_func(filter_func_ref: Optional[str]): """Check and import filter function from external python file.""" # Check if external filter code is defined filter_func = None filter_args = [] - filter_func_ref = filter_data.get("filter_func") - if filter_func_ref: try: filter_module, filter_function = filter_func_ref.rsplit(".") From 3c3445ca7d6e267022fb992c8ee0ced233deabee Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 00:47:14 +0200 Subject: [PATCH 23/29] =?UTF-8?q?=F0=9F=94=A7=20Disallow=20some=20Any=20co?= =?UTF-8?q?nstructs=20in=20typing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7a92aa9df..e0ec398c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,12 @@ strict = true show_error_codes = true implicit_reexport = true namespace_packages = true +disallow_any_generics = true +disallow_subclassing_any = true +# disallow_any_unimported = true +# disallow_any_explicit = true +# disallow_any_expr = true +# disallow_any_decorated = true [[tool.mypy.overrides]] module = [ From 96c75061c301a6e725e247caa66e672638a1a89e Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 01:10:10 +0200 Subject: [PATCH 24/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needextend`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 8 +++++++- sphinx_needs/directives/needextend.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e0ec398c9..c8b8da0b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,6 @@ module = [ 'sphinx_needs.data', 'sphinx_needs.directives.need', 'sphinx_needs.directives.needbar', - 'sphinx_needs.directives.needextend', 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', @@ -117,6 +116,13 @@ module = [ ] ignore_errors = true +[[tool.mypy.overrides]] +module = [ + 'sphinx_needs.directives.needextend' +] +# TODO dynamically overriding TypeDict keys is a bit tricky +disable_error_code = 'literal-required' + [build-system] requires = ["setuptools", "poetry_core>=1.0.8"] # setuptools for deps like plantuml build-backend = "poetry.core.masonry.api" diff --git a/sphinx_needs/directives/needextend.py b/sphinx_needs/directives/needextend.py index 8aa009578..1b2c7f75d 100644 --- a/sphinx_needs/directives/needextend.py +++ b/sphinx_needs/directives/needextend.py @@ -100,7 +100,7 @@ def process_needextend(app: Sphinx, doctree: nodes.document, fromdocname: str) - if need_filter in all_needs: need_filter = f'id == "{need_filter}"' # If it looks like a need id, but we haven't found one, raise an exception - elif re.fullmatch(needs_config.id_regex, need_filter): + elif need_filter is not None and re.fullmatch(needs_config.id_regex, need_filter): error = f"Provided id {need_filter} for needextend does not exist." if current_needextend["strict"]: raise NeedsInvalidFilter(error) @@ -190,7 +190,7 @@ def process_needextend(app: Sphinx, doctree: nodes.document, fromdocname: str) - removed_needextend_node(node) -def removed_needextend_node(node) -> None: +def removed_needextend_node(node: Needextend) -> None: """ # Remove needextend from docutils node-tree, so that no output gets generated for it. # Ok, this is really dirty. From 4338c0244a3f6a0614fe90fdccbc104f4a634039 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 01:15:38 +0200 Subject: [PATCH 25/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.needbar`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/data.py | 6 +++--- sphinx_needs/directives/needbar.py | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c8b8da0b6..c87eccde8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,6 @@ module = [ 'sphinx_needs.api.need', 'sphinx_needs.data', 'sphinx_needs.directives.need', - 'sphinx_needs.directives.needbar', 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index 370825018..fe3edb69f 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -243,9 +243,9 @@ class NeedsBarType(NeedsBaseDataType): ylabels_rotation: str separator: str stacked: bool - show_sum: bool - show_top_sum: bool - sum_rotation: bool + show_sum: None | bool + show_top_sum: None | bool + sum_rotation: None | str transpose: bool horizontal: bool style: str diff --git a/sphinx_needs/directives/needbar.py b/sphinx_needs/directives/needbar.py index 8e6fc5a1a..777b07525 100644 --- a/sphinx_needs/directives/needbar.py +++ b/sphinx_needs/directives/needbar.py @@ -279,14 +279,14 @@ def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, foun # 6. calculate index according to configuration and content size index = [] for row in range(len(local_data_number)): - line = [] + _line = [] for column in range(len(local_data_number[0])): if current_needbar["stacked"]: - line.append(column) + _line.append(column) else: value = row + column * len(local_data_number) + column - line.append(value) - index.append(line) + _line.append(value) + index.append(_line) # 7. styling and coloring style_previous_to_script_execution = matplotlib.rcParams From 8994677e73dffa59171669b75e230b2800cd36a1 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 01:25:06 +0200 Subject: [PATCH 26/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.data`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 12 +++++++++--- sphinx_needs/data.py | 21 +++------------------ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c87eccde8..fbbb71ddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = [ 'sphinx_needs.api.need', - 'sphinx_needs.data', 'sphinx_needs.directives.need', 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', @@ -117,10 +116,17 @@ ignore_errors = true [[tool.mypy.overrides]] module = [ - 'sphinx_needs.directives.needextend' + "sphinx_needs.directives.needextend" ] # TODO dynamically overriding TypeDict keys is a bit tricky -disable_error_code = 'literal-required' +disable_error_code = "literal-required" + +[[tool.mypy.overrides]] +module = [ + "sphinx_needs.data" +] +disable_error_code = ["attr-defined", "no-any-return"] + [build-system] requires = ["setuptools", "poetry_core>=1.0.8"] # setuptools for deps like plantuml diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index fe3edb69f..88706c59b 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -9,7 +9,7 @@ from typing import Literal, TypedDict except ImportError: # introduced in python 3.8 - from typing_extensions import Literal, TypedDict + from typing_extensions import Literal, TypedDict # type: ignore if TYPE_CHECKING: from docutils.nodes import Element, Text @@ -201,7 +201,7 @@ class NeedsInfoType(NeedsBaseDataType): max_amount: str service: str specific: str - type: str + # _type: str # type is already set in create_need updated_at: str user: str # OpenNeedsService @@ -293,7 +293,6 @@ class NeedsFilteredDiagramBaseType(NeedsFilteredBaseType): class NeedsExtractType(NeedsFilteredBaseType): """Data to extract needs from a document.""" - export_id: str layout: str style: str show_filters: bool @@ -311,24 +310,11 @@ class _NeedsFilterType(NeedsFilteredBaseType): show_filters: bool show_legend: bool layout: Literal["list", "table", "diagram"] - export_id: str class NeedsFlowType(NeedsFilteredDiagramBaseType): """Data for a single (filtered) flow chart.""" - caption: None | str - show_filters: bool - show_legend: bool - show_link_names: bool - debug: bool - config_names: str - config: str - scale: str - highlight: str - align: str - link_types: list[str] - class NeedsGanttType(NeedsFilteredDiagramBaseType): """Data for a single (filtered) gantt chart.""" @@ -350,7 +336,6 @@ class NeedsListType(NeedsFilteredBaseType): show_tags: bool show_status: bool show_filters: bool - export_id: str class NeedsPieType(NeedsBaseDataType): @@ -467,7 +452,7 @@ def get_or_create_workflow(self) -> NeedsWorkflowType: for link_type in self.env.app.config.needs_extra_links: self.env.needs_workflow["backlink_creation_{}".format(link_type["option"])] = False - return self.env.needs_workflow + return self.env.needs_workflow # type: ignore[return-value] def get_or_create_services(self) -> ServiceManager: """Get information about services. From ab5ea94209f2382b1718652380848359b41d5600 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 01:47:28 +0200 Subject: [PATCH 27/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.directives.need`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/directives/need.py | 49 +++++++++++++++++---------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fbbb71ddc..ed8133320 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = [ 'sphinx_needs.api.need', - 'sphinx_needs.directives.need', 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', diff --git a/sphinx_needs/directives/need.py b/sphinx_needs/directives/need.py index 43c4183a0..3b42e5d00 100644 --- a/sphinx_needs/directives/need.py +++ b/sphinx_needs/directives/need.py @@ -148,7 +148,7 @@ def run(self) -> Sequence[nodes.Node]: **need_extra_options, ) add_doc(env, self.docname) - return need_nodes + return need_nodes # type: ignore[no-any-return] def read_in_links(self, name: str) -> List[str]: # Get links @@ -178,7 +178,7 @@ def make_hashed_id(self, type_prefix: str, id_length: int) -> str: ) @property - def title_from_content(self): + def title_from_content(self) -> bool: return "title_from_content" in self.options or self.needs_config.title_from_content @property @@ -215,7 +215,7 @@ def _get_full_title(self) -> str: type="needs", location=(self.env.docname, self.lineno), ) - return self.arguments[0] + return self.arguments[0] # type: ignore[no-any-return] elif self.title_from_content: first_sentence = re.split(r"[.\n]", "\n".join(self.content))[0] if not first_sentence: @@ -229,7 +229,9 @@ def _get_full_title(self) -> str: return "" -def get_sections_and_signature_and_needs(need_node) -> Tuple[List[str], Optional[nodes.Text], List[str]]: +def get_sections_and_signature_and_needs( + need_node: Optional[nodes.Node], +) -> Tuple[List[str], Optional[nodes.Text], List[str]]: """Gets the hierarchy of the section nodes as a list starting at the section of the current need and then its parent sections""" sections = [] @@ -238,7 +240,7 @@ def get_sections_and_signature_and_needs(need_node) -> Tuple[List[str], Optional current_node = need_node while current_node: if isinstance(current_node, nodes.section): - title = typing.cast(str, current_node.children[0].astext()) # type: ignore[no-untyped-call] + title = typing.cast(str, current_node.children[0].astext()) # If using auto-section numbering, then Sphinx inserts # multiple non-breaking space unicode characters into the title # we'll replace those with a simple space to make them easier to @@ -328,13 +330,13 @@ def add_sections(app: Sphinx, doctree: nodes.document) -> None: need_info["parent_need"] = parent_needs[0] -def previous_sibling(node): +def previous_sibling(node: nodes.Node) -> Optional[nodes.Node]: """Return preceding sibling node or ``None``.""" try: - i = node.parent.index(node) + i = node.parent.index(node) # type: ignore except AttributeError: return None - return node.parent[i - 1] if i > 0 else None + return node.parent[i - 1] if i > 0 else None # type: ignore @profile("NEED_PROCESS") @@ -352,7 +354,8 @@ def process_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) - needs_config = NeedsSphinxConfig(app.config) if not needs_config.include_needs: for node in doctree.findall(Need): - node.parent.remove(node) + if node.parent is not None: + node.parent.remove(node) # type: ignore return builder = unwrap(app.builder) @@ -400,7 +403,7 @@ def process_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) - @profile("NEED_PRINT") -def print_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str, found_needs_nodes: list) -> None: +def print_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str, found_needs_nodes: List[Need]) -> None: """ Finally creates the need-node in the docutils node-tree. @@ -445,7 +448,7 @@ def check_links(env: BuildEnvironment) -> None: for link_type in extra_links: dead_links_allowed = link_type.get("allow_dead_links", False) need_link_value = ( - [need[link_type["option"]]] if isinstance(need[link_type["option"]], str) else need[link_type["option"]] + [need[link_type["option"]]] if isinstance(need[link_type["option"]], str) else need[link_type["option"]] # type: ignore ) for link in need_link_value: if "." in link: @@ -465,7 +468,7 @@ def check_links(env: BuildEnvironment) -> None: workflow["links_checked"] = True -def create_back_links(env: BuildEnvironment, option) -> None: +def create_back_links(env: BuildEnvironment, option: str) -> None: """ Create back-links in all found needs. But do this only once, as all needs are already collected and this sorting is for all @@ -476,12 +479,12 @@ def create_back_links(env: BuildEnvironment, option) -> None: data = SphinxNeedsData(env) workflow = data.get_or_create_workflow() option_back = f"{option}_back" - if workflow[f"backlink_creation_{option}"]: + if workflow[f"backlink_creation_{option}"]: # type: ignore[literal-required] return needs = data.get_or_create_needs() for key, need in needs.items(): - need_link_value = [need[option]] if isinstance(need[option], str) else need[option] + need_link_value = [need[option]] if isinstance(need[option], str) else need[option] # type: ignore[literal-required] for link in need_link_value: link_main = link.split(".")[0] try: @@ -490,16 +493,16 @@ def create_back_links(env: BuildEnvironment, option) -> None: link_part = None if link_main in needs: - if key not in needs[link_main][option_back]: - needs[link_main][option_back].append(key) + if key not in needs[link_main][option_back]: # type: ignore[literal-required] + needs[link_main][option_back].append(key) # type: ignore[literal-required] # Handling of links to need_parts inside a need if link_part and link_part in needs[link_main]["parts"]: if option_back not in needs[link_main]["parts"][link_part].keys(): - needs[link_main]["parts"][link_part][option_back] = [] - needs[link_main]["parts"][link_part][option_back].append(key) + needs[link_main]["parts"][link_part][option_back] = [] # type: ignore[literal-required] + needs[link_main]["parts"][link_part][option_back].append(key) # type: ignore[literal-required] - workflow[f"backlink_creation_{option}"] = True + workflow[f"backlink_creation_{option}"] = True # type: ignore[literal-required] def _fix_list_dyn_func(list: List[str]) -> List[str]: @@ -547,7 +550,7 @@ def _fix_list_dyn_func(list: List[str]) -> List[str]: # Need-Node. -def html_visit(self, node) -> None: +def html_visit(self: Any, node: nodes.Node) -> None: """ Visitor method for Need-node of builder 'html'. Does only wrap the Need-content into an extra
with class=need @@ -555,13 +558,13 @@ def html_visit(self, node) -> None: self.body.append(self.starttag(node, "div", "", CLASS="need")) -def html_depart(self, node) -> None: +def html_depart(self: Any, node: nodes.Node) -> None: self.body.append("
") -def latex_visit(self, node) -> None: +def latex_visit(self: Any, node: nodes.Node) -> None: pass -def latex_depart(self, node) -> None: +def latex_depart(self: Any, node: nodes.Node) -> None: pass From ce9ac8570fceba4277f9f51692d618495b8d10af Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 02:51:28 +0200 Subject: [PATCH 28/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.utils`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/functions/functions.py | 2 +- sphinx_needs/utils.py | 98 ++++++++++++++++++----------- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ed8133320..0db313919 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,6 @@ module = [ 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', 'sphinx_needs.layout', - 'sphinx_needs.utils', ] ignore_errors = true diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index cf355d3a1..f184bc2da 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -257,7 +257,7 @@ def resolve_variants_options(env: BuildEnvironment): needs = data.get_or_create_needs() for need in needs.values(): # Data to use as filter context. - need_context: Dict = {**need} + need_context: Dict[str, Any] = {**need} need_context.update(**needs_config.filter_data) # Add needs_filter_data to filter context _sphinx_tags = env.app.builder.tags.tags # Get sphinx tags need_context.update(**_sphinx_tags) # Add sphinx tags to filter context diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index 6da53e8e1..2ad3db321 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -5,11 +5,22 @@ import re from functools import reduce, wraps from re import Pattern -from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, +) from urllib.parse import urlparse from docutils import nodes -from jinja2 import BaseLoader, Environment, Template +from jinja2 import Environment, Template from matplotlib.figure import FigureBase from sphinx.application import BuildEnvironment, Sphinx @@ -18,9 +29,23 @@ from sphinx_needs.defaults import NEEDS_PROFILING from sphinx_needs.logging import get_logger +try: + from typing import TypedDict +except ImportError: + from typing_extensions import TypedDict + +if TYPE_CHECKING: + from sphinx_needs.functions.functions import DynamicFunction + logger = get_logger(__name__) -NEEDS_FUNCTIONS = {} + +class NeedFunctionsType(TypedDict): + name: str + function: "DynamicFunction" + + +NEEDS_FUNCTIONS: Dict[str, NeedFunctionsType] = {} # List of internal need option names. They should not be used by or presented to user. INTERNALS = [ @@ -116,14 +141,15 @@ def row_col_maker( for v in needs_config.string_links.values(): needs_string_links_option.extend(v["options"]) - if need_key in need_info and need_info[need_key] is not None: - if isinstance(need_info[need_key], (list, set)): - data = need_info[need_key] - elif isinstance(need_info[need_key], str) and need_key in needs_string_links_option: - data = re.split(r",|;", need_info[need_key]) + if need_key in need_info and need_info[need_key] is not None: # type: ignore[literal-required] + value = need_info[need_key] # type: ignore[literal-required] + if isinstance(value, (list, set)): + data = value + elif isinstance(value, str) and need_key in needs_string_links_option: + data = re.split(r",|;", value) data = [i.strip() for i in data if len(i) != 0] else: - data = [need_info[need_key]] + data = [value] for index, datum in enumerate(data): link_id = datum @@ -138,10 +164,8 @@ def row_col_maker( link_string_list = {} for link_name, link_conf in needs_config.string_links.items(): link_string_list[link_name] = { - "url_template": Environment(loader=BaseLoader, autoescape=True).from_string(link_conf["link_url"]), - "name_template": Environment(loader=BaseLoader, autoescape=True).from_string( - link_conf["link_name"] - ), + "url_template": Environment(autoescape=True).from_string(link_conf["link_url"]), + "name_template": Environment(autoescape=True).from_string(link_conf["link_name"]), "regex_compiled": re.compile(link_conf["regex"]), "options": link_conf["options"], "name": link_name, @@ -170,6 +194,7 @@ def row_col_maker( if make_ref: if need_info["is_external"]: + assert need_info["external_url"] is not None, "external_url must be set for external needs" ref_col["refuri"] = check_and_calc_base_url_rel_path(need_info["external_url"], fromdocname) ref_col["classes"].append(need_info["external_css"]) row_col["classes"].append(need_info["external_css"]) @@ -179,6 +204,7 @@ def row_col_maker( elif ref_lookup: temp_need = all_needs[link_id] if temp_need["is_external"]: + assert temp_need["external_url"] is not None, "external_url must be set for external needs" ref_col["refuri"] = check_and_calc_base_url_rel_path(temp_need["external_url"], fromdocname) ref_col["classes"].append(temp_need["external_css"]) row_col["classes"].append(temp_need["external_css"]) @@ -263,9 +289,9 @@ def profile(keyword: str) -> Callable[[FuncT], FuncT]: Activation only happens, if given keyword is part of ``needs_profiling``. """ - def inner(func): + def inner(func): # type: ignore @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs): # type: ignore with cProfile.Profile() as pr: result = func(*args, **kwargs) @@ -304,11 +330,11 @@ def check_and_calc_base_url_rel_path(external_url: str, fromdocname: str) -> str return ref_uri -def check_and_get_external_filter_func(filter_func_ref: Optional[str]): +def check_and_get_external_filter_func(filter_func_ref: Optional[str]) -> Tuple[Any, str]: """Check and import filter function from external python file.""" # Check if external filter code is defined filter_func = None - filter_args = [] + filter_args = "" if filter_func_ref: try: @@ -317,23 +343,25 @@ def check_and_get_external_filter_func(filter_func_ref: Optional[str]): logger.warning( f'Filter function not valid "{filter_func_ref}". Example: my_module:my_func [needs]', type="needs" ) - return [] # No needs were found because of invalid filter function + return filter_func, filter_args result = re.search(r"^(\w+)(?:\((.*)\))*$", filter_function) + if not result: + return filter_func, filter_args filter_function = result.group(1) - filter_args = result.group(2) or [] + filter_args = result.group(2) or "" try: final_module = importlib.import_module(filter_module) filter_func = getattr(final_module, filter_function) except Exception: logger.warning(f"Could not import filter function: {filter_func_ref} [needs]", type="needs") - return [] + return filter_func, filter_args return filter_func, filter_args -def jinja_parse(context: Dict, jinja_string: str) -> str: +def jinja_parse(context: Dict[str, Any], jinja_string: str) -> str: """ Function to parse mapping options set to a string containing jinja template format. @@ -393,7 +421,7 @@ def save_matplotlib_figure(app: Sphinx, figure: FigureBase, basename: str, fromd return image_node -def dict_get(root, items, default=None) -> Any: +def dict_get(root: Dict[str, Any], items: Any, default: Any = None) -> Any: """ Access a nested object in root by item sequence. @@ -411,7 +439,7 @@ def dict_get(root, items, default=None) -> Any: def match_string_link( - text_item: str, data: str, need_key: str, matching_link_confs: List[Dict], render_context: Dict[str, Any] + text_item: str, data: str, need_key: str, matching_link_confs: List[Dict[str, Any]], render_context: Dict[str, Any] ) -> Any: try: link_name = None @@ -438,23 +466,22 @@ def match_string_link( return ref_item -def match_variants(option_value: Union[str, List], keywords: Dict, needs_variants: Dict) -> Union[str, List, None]: +def match_variants( + option_value: Union[str, List[str]], keywords: Dict[str, Any], needs_variants: Dict[str, str] +) -> Union[None, str, List[str]]: """ Function to handle variant option management. :param option_value: Value assigned to an option - :type option_value: Union[str, List] :param keywords: Data to use as filtering context - :type keywords: Dict :param needs_variants: Needs variants data set in users conf.py - :type needs_variants: Dict :return: A string, list, or None to be used as value for option. :rtype: Union[str, List, None] """ def variant_handling( - variant_definitions: List, variant_data: Dict, variant_pattern: Pattern - ) -> Union[str, List, None]: + variant_definitions: List[str], variant_data: Dict[str, Any], variant_pattern: Pattern # type: ignore[type-arg] + ) -> Optional[str]: filter_context = variant_data # filter_result = [] no_variants_in_option = False @@ -505,8 +532,8 @@ def variant_handling( # Handling multiple variant definitions if isinstance(option_value, str): - multiple_variants: List = variant_splitting.split(rf"""{option_value}""") - multiple_variants: List = [ + multiple_variants: List[str] = variant_splitting.split(rf"""{option_value}""") + multiple_variants = [ re.sub(r"^([;, ]+)|([;, ]+$)", "", i) for i in multiple_variants if i not in (None, ";", "", " ") ] if len(multiple_variants) == 1 and not variant_rule_matching.search(multiple_variants[0]): @@ -516,7 +543,7 @@ def variant_handling( return option_value return new_option_value elif isinstance(option_value, (list, set, tuple)): - multiple_variants: List = list(option_value) + multiple_variants = list(option_value) # In case an option value is a list (:tags: open; close), and does not contain any variant definition, # then return the unmodified value # options = all([bool(not variant_rule_matching.search(i)) for i in multiple_variants]) @@ -574,10 +601,9 @@ def node_match(node_types: Union[Type[nodes.Element], List[Type[nodes.Element]]] :param node_types: List of docutils node types :return: function, which can be used as constraint-function for docutils findall() """ - if not isinstance(node_types, list): - node_types = [node_types] + node_types_list = node_types if isinstance(node_types, list) else [node_types] - def condition(node, node_types=node_types): + def condition(node: nodes.Node, node_types: List[Type[nodes.Element]] = node_types_list) -> bool: return any(isinstance(node, x) for x in node_types) return condition @@ -619,7 +645,7 @@ def _is_valid(link_type: str) -> bool: def get_scale(options: Dict[str, Any], location: Any) -> str: """Get scale for diagram, from directive option.""" - scale = options.get("scale", "100").replace("%", "") + scale: str = options.get("scale", "100").replace("%", "") if not scale.isdigit(): logger.warning( f'scale value must be a number. "{scale}" found [needs]', From 146bc68e03e75090e22adad9e827ad3619d2447e Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 29 Aug 2023 10:48:20 +0200 Subject: [PATCH 29/29] =?UTF-8?q?=F0=9F=94=A7=20Add=20strict=20typing=20fo?= =?UTF-8?q?r=20`sphinx=5Fneeds.layout`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - sphinx_needs/layout.py | 150 ++++++++++++++++++++++------------------- 2 files changed, 81 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0db313919..b931398e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,6 @@ module = [ 'sphinx_needs.api.need', 'sphinx_needs.directives.needuml', 'sphinx_needs.functions.functions', - 'sphinx_needs.layout', ] ignore_errors = true diff --git a/sphinx_needs/layout.py b/sphinx_needs/layout.py index cf47c9953..b8547b28a 100644 --- a/sphinx_needs/layout.py +++ b/sphinx_needs/layout.py @@ -9,7 +9,7 @@ from contextlib import suppress from functools import lru_cache from optparse import Values -from typing import List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple, Union from urllib.parse import urlparse import requests @@ -18,18 +18,20 @@ from docutils.parsers.rst import Parser, languages from docutils.parsers.rst.states import Inliner, Struct from docutils.utils import new_document -from jinja2 import BaseLoader, Environment +from jinja2 import Environment from sphinx.application import Sphinx from sphinx.environment.collectors.asset import DownloadFileCollector, ImageCollector from sphinx_needs.config import NeedsSphinxConfig -from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.data import NeedsInfoType, SphinxNeedsData from sphinx_needs.debug import measure_time from sphinx_needs.utils import INTERNALS, match_string_link, unwrap @measure_time("need") -def create_need(need_id: str, app: Sphinx, layout=None, style=None, docname: Optional[str] = None) -> nodes.container: +def create_need( + need_id: str, app: Sphinx, layout: Optional[str] = None, style: Optional[str] = None, docname: Optional[str] = None +) -> nodes.container: """ Creates a new need-node for a given layout. @@ -62,7 +64,9 @@ def create_need(need_id: str, app: Sphinx, layout=None, style=None, docname: Opt # We must create a standalone copy of the content_node, as it may be reused several time # (multiple needextract for the same need) and the Sphinx ImageTransformator add location specific # uri to some nodes, which are not valid for all locations. - node_inner = needs[need_id]["content_node"].deepcopy() + content_node = needs[need_id]["content_node"] + assert content_node is not None, f"Need {need_id} has no content node." + node_inner = content_node.deepcopy() # Rerun some important Sphinx collectors for need-content coming from "needsexternal". # This is needed, as Sphinx needs to know images and download paths. @@ -72,8 +76,8 @@ def create_need(need_id: str, app: Sphinx, layout=None, style=None, docname: Opt # Overwrite the docname, which must be the original one from the reused need, as all used paths are relative # to the original location, not to the current document. env.temp_data["docname"] = need_data["docname"] # Dirty, as in this phase normally no docname is set anymore in env - ImageCollector().process_doc(app, node_inner) - DownloadFileCollector().process_doc(app, node_inner) + ImageCollector().process_doc(app, node_inner) # type: ignore[arg-type] + DownloadFileCollector().process_doc(app, node_inner) # type: ignore[arg-type] del env.temp_data["docname"] # Be sure our env is as it was before @@ -102,7 +106,7 @@ def create_need(need_id: str, app: Sphinx, layout=None, style=None, docname: Opt return node_container -def replace_pending_xref_refdoc(node, new_refdoc: str) -> None: +def replace_pending_xref_refdoc(node: nodes.Element, new_refdoc: str) -> None: """ Overwrites the refdoc attribute of all pending_xref nodes. This is needed, if a doctree with references gets copied used somewhereelse in the documentation. @@ -117,11 +121,13 @@ def replace_pending_xref_refdoc(node, new_refdoc: str) -> None: node.attributes["refdoc"] = new_refdoc else: for child in node.children: - replace_pending_xref_refdoc(child, new_refdoc) + replace_pending_xref_refdoc(child, new_refdoc) # type: ignore[arg-type] @measure_time("need") -def build_need(layout, node, app: Sphinx, style=None, fromdocname: Optional[str] = None) -> None: +def build_need( + layout: str, node: nodes.Element, app: Sphinx, style: Optional[str] = None, fromdocname: Optional[str] = None +) -> None: """ Builds a need based on a given layout for a given need-node. @@ -137,31 +143,24 @@ def build_need(layout, node, app: Sphinx, style=None, fromdocname: Optional[str] ------ custom layout nodes The level structure must be kept, otherwise docutils can not handle it! - - :param layout: - :param node: - :param app: - :param style: - :param fromdocname: - :return: """ env = app.builder.env needs = SphinxNeedsData(env).get_or_create_needs() node_container = nodes.container() - need_layout = layout need_id = node.attributes["ids"][0] need_data = needs[need_id] if need_data["hide"]: - node.parent.replace(node, []) + if node.parent: + node.parent.replace(node, []) # type: ignore return if fromdocname is None: fromdocname = need_data["docname"] - lh = LayoutHandler(app, need_data, need_layout, node, style, fromdocname) + lh = LayoutHandler(app, need_data, layout, node, style, fromdocname) new_need_node = lh.get_need_table() node_container.append(new_need_node) @@ -171,14 +170,14 @@ def build_need(layout, node, app: Sphinx, style=None, fromdocname: Optional[str] # We need to replace the current need-node (containing content only) with our new table need node. # node.parent.replace(node, node_container) - node.parent.replace(node, node_container) + node.parent.replace(node, node_container) # type: ignore @lru_cache(1) def _generate_inline_parser() -> Tuple[Values, Inliner]: doc_settings = OptionParser(components=(Parser,)).get_default_values() inline_parser = Inliner() - inline_parser.init_customizations(doc_settings) + inline_parser.init_customizations(doc_settings) # type: ignore return doc_settings, inline_parser @@ -187,13 +186,21 @@ class LayoutHandler: Cares about the correct layout handling """ - def __init__(self, app: Sphinx, need, layout, node, style=None, fromdocname: Optional[str] = None) -> None: + def __init__( + self, + app: Sphinx, + need: NeedsInfoType, + layout: str, + node: nodes.Element, + style: Optional[str] = None, + fromdocname: Optional[str] = None, + ) -> None: self.app = app self.need = need - self.config = NeedsSphinxConfig(app.config) + self.needs_config = NeedsSphinxConfig(app.config) self.layout_name = layout - available_layouts = self.config.layouts + available_layouts = self.needs_config.layouts if self.layout_name not in available_layouts.keys(): raise SphinxNeedLayoutException( 'Given layout "{}" is unknown for need {}. Registered layouts are: {}'.format( @@ -212,9 +219,9 @@ def __init__(self, app: Sphinx, need, layout, node, style=None, fromdocname: Opt # For ReadTheDocs Theme we need to add 'rtd-exclude-wy-table'. classes = ["need", "needs_grid_" + self.layout["grid"], "needs_layout_" + self.layout_name] - classes.extend(self.config.table_classes) + classes.extend(self.needs_config.table_classes) - self.style = style or self.need["style"] or getattr(self.app.config, "needs_default_style", None) + self.style = style or self.need["style"] or self.needs_config.default_style if self.style: for style in self.style.strip().split(","): @@ -294,7 +301,7 @@ def __init__(self, app: Sphinx, need, layout, node, style=None, fromdocname: Opt inliner=None, ) - self.functions = { + self.functions: Dict[str, Callable[..., Union[None, nodes.Node, List[nodes.Node]]]] = { "meta": self.meta, "meta_all": self.meta_all, "meta_links": self.meta_links, @@ -312,10 +319,10 @@ def __init__(self, app: Sphinx, need, layout, node, style=None, fromdocname: Opt # This would lead to deepcopy()-errors, as needs_string_links gets some "pickled" and jinja Environment is # too complex for this. self.string_links = {} - for link_name, link_conf in self.config.string_links.items(): + for link_name, link_conf in self.needs_config.string_links.items(): self.string_links[link_name] = { - "url_template": Environment(loader=BaseLoader).from_string(link_conf["link_url"]), - "name_template": Environment(loader=BaseLoader).from_string(link_conf["link_name"]), + "url_template": Environment().from_string(link_conf["link_url"]), + "name_template": Environment().from_string(link_conf["link_name"]), "regex_compiled": re.compile(link_conf["regex"]), "options": link_conf["options"], "name": link_name, @@ -331,7 +338,7 @@ def get_need_table(self) -> nodes.table: if callable(func): func() else: - func["func"](**func["configs"]) + func["func"](**func["configs"]) # type: ignore[index] return self.node_table @@ -366,12 +373,12 @@ def _parse(self, line: str) -> List[nodes.Node]: :param line: string to parse :return: nodes """ - result, message = self.inline_parser.parse(line, 0, self.doc_memo, self.dummy_doc) + result, message = self.inline_parser.parse(line, 0, self.doc_memo, self.dummy_doc) # type: ignore if message: raise SphinxNeedLayoutException(message) - return result + return result # type: ignore[no-any-return] - def _func_replace(self, section_nodes): + def _func_replace(self, section_nodes: List[nodes.Node]) -> List[nodes.Node]: """ Replaces a function definition like ``<>`` with the related docutils nodes. @@ -382,11 +389,12 @@ def _func_replace(self, section_nodes): :return: docutils nodes """ return_nodes = [] + result: Union[None, nodes.Node, List[nodes.Node]] for node in section_nodes: if not isinstance(node, nodes.Text): for child in node.children: new_child = self._func_replace([child]) - node.replace(child, new_child) + node.replace(child, new_child) # type: ignore[attr-defined] return_nodes.append(node) else: node_str = str(node) @@ -462,18 +470,18 @@ def _func_replace(self, section_nodes): return_nodes.append(node_line) return return_nodes - def _replace_place_holder(self, data): + def _replace_place_holder(self, data: str) -> str: replace_items = re.findall(r"{{(.*)}}", data) for item in replace_items: - if item not in self.need.keys(): + if item not in self.need: raise SphinxNeedLayoutException(item) # To escape { we need to use 2 of them. # So {{ becomes {{{{ replace_string = f"{{{{{item}}}}}" - data = data.replace(replace_string, self.need[item]) + data = data.replace(replace_string, self.need[item]) # type: ignore[literal-required] return data - def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False): + def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False) -> nodes.inline: """ Returns the specific metadata of a need inside docutils nodes. Usage:: @@ -493,7 +501,7 @@ def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False label_node += prefix_node data_container.append(label_node) try: - data = self.need[name] + data = self.need[name] # type: ignore[literal-required] except KeyError: data = "" @@ -509,7 +517,7 @@ def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False # data_node.append(nodes.Text(data) # data_container.append(data_node) needs_string_links_option: List[str] = [] - for v in self.config.string_links.values(): + for v in self.needs_config.string_links.values(): needs_string_links_option.extend(v["options"]) if name in needs_string_links_option: @@ -529,7 +537,7 @@ def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False data=datum, need_key=name, matching_link_confs=matching_link_confs, - render_context=self.config.render_context, + render_context=self.needs_config.render_context, ) else: # Normal text handling @@ -560,7 +568,7 @@ def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False return data_container - def meta_id(self): + def meta_id(self) -> nodes.inline: """ Returns the current need id as clickable and linked reference. @@ -591,11 +599,11 @@ def meta_all( self, prefix: str = "", postfix: str = "", - exclude=None, + exclude: Optional[List[str]] = None, no_links: bool = False, defaults: bool = True, show_empty: bool = False, - ): + ) -> nodes.inline: """ ``meta_all()`` excludes by default the output of: ``docname``, ``lineno``, ``refid``, ``content``, ``collapse``, ``parts``, ``id_parent``, @@ -635,8 +643,8 @@ def meta_all( exclude += default_excludes if no_links: - link_names = [x["option"] for x in self.config.extra_links] - link_names += [x["option"] + "_back" for x in self.config.extra_links] + link_names = [x["option"] for x in self.needs_config.extra_links] + link_names += [x["option"] + "_back" for x in self.needs_config.extra_links] exclude += link_names data_container = nodes.inline() for data in self.need.keys(): @@ -657,7 +665,7 @@ def meta_all( return data_container - def meta_links(self, name: str, incoming: bool = False): + def meta_links(self, name: str, incoming: bool = False) -> nodes.inline: """ Documents the set links of a given link type. The documented links are all full clickable links to the target needs. @@ -667,7 +675,7 @@ def meta_links(self, name: str, incoming: bool = False): :return: docutils nodes """ data_container = nodes.inline(classes=[name]) - if name not in [x["option"] for x in self.config.extra_links]: + if name not in [x["option"] for x in self.needs_config.extra_links]: raise SphinxNeedLayoutException(f"Invalid link name {name} for link-type") # if incoming: @@ -686,7 +694,9 @@ def meta_links(self, name: str, incoming: bool = False): data_container.append(node_links) return data_container - def meta_links_all(self, prefix: str = "", postfix: str = "", exclude=None): + def meta_links_all( + self, prefix: str = "", postfix: str = "", exclude: Optional[List[str]] = None + ) -> List[nodes.line]: """ Documents all used link types for the current need automatically. @@ -697,9 +707,9 @@ def meta_links_all(self, prefix: str = "", postfix: str = "", exclude=None): """ exclude = exclude or [] data_container = [] - for link_type in self.config.extra_links: + for link_type in self.needs_config.extra_links: type_key = link_type["option"] - if self.need[type_key] and type_key not in exclude: + if self.need[type_key] and type_key not in exclude: # type: ignore[literal-required] outgoing_line = nodes.line() outgoing_label = prefix + "{}:".format(link_type["outgoing"]) + postfix + " " outgoing_line += self._parse(outgoing_label) @@ -707,7 +717,7 @@ def meta_links_all(self, prefix: str = "", postfix: str = "", exclude=None): data_container.append(outgoing_line) type_key = link_type["option"] + "_back" - if self.need[type_key] and type_key not in exclude: + if self.need[type_key] and type_key not in exclude: # type: ignore[literal-required] incoming_line = nodes.line() incoming_label = prefix + "{}:".format(link_type["incoming"]) + postfix + " " incoming_line += self._parse(incoming_label) @@ -718,11 +728,11 @@ def meta_links_all(self, prefix: str = "", postfix: str = "", exclude=None): def image( self, - url, - height=None, - width=None, - align=None, - no_link=False, + url: str, + height: Optional[str] = None, + width: Optional[str] = None, + align: Optional[str] = None, + no_link: bool = False, prefix: str = "", is_external: bool = False, img_class: str = "", @@ -802,7 +812,7 @@ def image( elif url.startswith("field:"): field = url.split(":")[1] try: - value = self.need[field] + value = self.need[field] # type: ignore[literal-required] except KeyError: value = "" @@ -860,8 +870,8 @@ def link( url: str, text: Optional[str] = None, image_url: Optional[str] = None, - image_height=None, - image_width=None, + image_height: Optional[str] = None, + image_width: Optional[str] = None, prefix: str = "", is_dynamic: bool = False, ) -> nodes.inline: @@ -894,7 +904,7 @@ def link( if is_dynamic: try: - url = self.need[url] + url = self.need[url] # type: ignore[literal-required] except KeyError: url = "" @@ -973,11 +983,11 @@ def collapse_button( def permalink( self, image_url: Optional[str] = None, - image_height=None, - image_width=None, + image_height: Optional[str] = None, + image_width: Optional[str] = None, text: Optional[str] = None, prefix: str = "", - ): + ) -> nodes.inline: """ Shows a permanent link to the need. Link can be a text, an image or both @@ -1000,7 +1010,7 @@ def permalink( image_url = "icon:share-2" image_width = "17px" - permalink = self.config.permalink_file + permalink = self.needs_config.permalink_file id = self.need["id"] docname = self.need["docname"] permalink_url = "" @@ -1017,7 +1027,9 @@ def permalink( prefix=prefix, ) - def _grid_simple(self, colwidths, side_left, side_right, footer): + def _grid_simple( + self, colwidths: List[int], side_left: Union[bool, str], side_right: Union[bool, str], footer: bool + ) -> None: """ Creates most "simple" grid layouts. Side parts and footer can be activated via config. @@ -1190,7 +1202,7 @@ def _grid_complex(self) -> None: # Construct table node_tgroup += self.node_tbody - def _grid_content(self, colwidths, side_left, side_right, footer): + def _grid_content(self, colwidths: List[int], side_left: bool, side_right: bool, footer: bool) -> None: """ Creates most "content" based grid layouts. Side parts and footer can be activated via config.