Skip to content

Commit 6f90e99

Browse files
authored
♻️ Auto-compute certain need fields (#1496)
A number of need fields are actually data that is based on other data: - `is_need` / `is_part` are based on whether the item is a `NeedItem` or `NeedPartItem` instance - `is_modified` is based on if `modifications > 0` - `id_parent` / `id_complete` are based on the `id` (and part `id` if `NeedPartItem`) - `section_name` is the first item in `sections` - `parent_need` is the first item in the `parent_needs` links - `constraints` is the keys of `constraints_results` - `constraints_passed` is derived from `constraints_results` These are mainly there for user convenience when writing filters, but are not necessary to specify independently, and could lead to inconcistencies in the data. Therefore, these fields are now auto-computed when initialiasing/modifying the need, rather than being allowed to be specified / modified independantly.
1 parent f3a293c commit 6f90e99

File tree

15 files changed

+959
-409
lines changed

15 files changed

+959
-409
lines changed

docs/api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Data
4141
:members: NeedItem, NeedPartItem, NeedItemSourceProtocol
4242

4343
.. automodule:: sphinx_needs.data
44-
:members: NeedsInfoType, NeedsSourceInfoType, NeedsMutable, NeedsPartType
44+
:members: NeedsInfoType, NeedsInfoComputedType, NeedsSourceInfoType, NeedsMutable, NeedsPartType
4545

4646
Views
4747
-----

sphinx_needs/api/need.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import re
66
import warnings
7-
from collections.abc import Iterable, Iterator
7+
from collections.abc import Iterable, Iterator, Sequence
88
from contextlib import contextmanager
99
from pathlib import Path
1010
from typing import Any, TypeVar
@@ -64,7 +64,7 @@ def generate_need(
6464
parts: dict[str, NeedsPartType] | None = None,
6565
arch: dict[str, str] | None = None,
6666
signature: None | str = None,
67-
sections: list[str] | None = None,
67+
sections: Sequence[str] | None = None,
6868
jinja_content: bool | None = None,
6969
hide: None | bool = None,
7070
collapse: None | bool = None,
@@ -321,10 +321,6 @@ def generate_need(
321321
links_no_defaults, needs_config, defaults_ctx, defaults_extras, defaults_links
322322
)
323323
_copy_links(links, needs_config)
324-
# ensure parent_need is consistent with parent_needs
325-
parent_need = (
326-
parent_needs[0] if (parent_needs := links.get("parent_needs")) else None
327-
)
328324

329325
# Add the need and all needed information
330326
needs_data: NeedsInfoType = {
@@ -339,9 +335,7 @@ def generate_need(
339335
"type_style": need_type_data.get("style") or "node",
340336
"status": status,
341337
"tags": tags,
342-
"constraints": constraints,
343-
"constraints_passed": True,
344-
"constraints_results": {},
338+
"constraints_results": {k: {} for k in constraints},
345339
"constraints_error": None,
346340
"id": need_id,
347341
"title": title,
@@ -355,19 +349,12 @@ def generate_need(
355349
"hide": hide,
356350
"jinja_content": jinja_content or False,
357351
"parts": parts or {},
358-
"is_part": False,
359-
"is_need": True,
360-
"id_parent": need_id,
361-
"id_complete": need_id,
362352
"external_css": external_css or "external_link",
363-
"is_modified": False,
364353
"modifications": 0,
365354
"has_dead_links": False,
366355
"has_forbidden_dead_links": False,
367-
"sections": sections or [],
368-
"section_name": sections[0] if sections else None,
356+
"sections": tuple(sections or ()),
369357
"signature": signature,
370-
"parent_need": parent_need,
371358
}
372359

373360
needs_info = NeedItem(
@@ -653,7 +640,7 @@ def _create_need_node(
653640
)
654641

655642
# Extract plantuml diagrams and store needumls with keys in arch, e.g. need_info['arch']['diagram']
656-
data["arch"] = {}
643+
arch = {}
657644
node_need_needumls_without_key = []
658645
node_need_needumls_key_names = []
659646
for child in node_need.children:
@@ -669,7 +656,7 @@ def _create_need_node(
669656
f"Inside need: {data['id']}, found duplicate Needuml option key name: {key_name}"
670657
)
671658
else:
672-
data["arch"][key_name] = needuml["content"]
659+
arch[key_name] = needuml["content"]
673660
node_need_needumls_key_names.append(key_name)
674661
else:
675662
node_need_needumls_without_key.append(needuml)
@@ -678,9 +665,10 @@ def _create_need_node(
678665

679666
# only store the first needuml-node which has no key option under diagram
680667
if node_need_needumls_without_key:
681-
data["arch"]["diagram"] = node_need_needumls_without_key[0]["content"]
668+
arch["diagram"] = node_need_needumls_without_key[0]["content"]
669+
670+
data["arch"] = arch
682671

683-
data["parts"] = {}
684672
need_parts = find_parts(node_need)
685673
update_need_with_parts(env, data, need_parts)
686674

sphinx_needs/data.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ class CoreFieldParameters(TypedDict):
328328
},
329329
"constraints": {
330330
"description": "List of constraint names, which are defined for this need.",
331-
"schema": {"type": "array", "items": {"type": "string"}, "default": []},
331+
"schema": {"type": "array", "items": {"type": "string"}, "default": ()},
332332
"allow_default": "str_list",
333333
"allow_df": True,
334334
"allow_extend": True,
@@ -361,7 +361,7 @@ class CoreFieldParameters(TypedDict):
361361
},
362362
"sections": {
363363
"description": "Sections of the need.",
364-
"schema": {"type": "array", "items": {"type": "string"}, "default": []},
364+
"schema": {"type": "array", "items": {"type": "string"}, "default": ()},
365365
"exclude_import": True,
366366
},
367367
"section_name": {
@@ -424,10 +424,6 @@ class NeedsInfoType(TypedDict):
424424
style: None | str
425425
"""Comma-separated list of CSS classes (all appended by `needs_style_`)."""
426426

427-
# TODO why is it called arch?
428-
arch: dict[str, str]
429-
"""Mapping of uml key to uml content."""
430-
431427
external_css: str
432428
"""CSS class name, added to the external reference."""
433429

@@ -439,21 +435,11 @@ class NeedsInfoType(TypedDict):
439435
"""Hexadecimal color code of the type."""
440436
type_style: str
441437

442-
is_modified: bool
443-
"""Whether the need was modified by needextend."""
444438
modifications: int
445439
"""Number of modifications by needextend."""
446440

447-
# used to distinguish a part from a need
448-
is_need: bool
449-
is_part: bool
450441
# Mapping of parts, a.k.a. sub-needs, IDs to data that overrides the need's data
451-
parts: dict[str, NeedsPartType]
452-
# additional information required for compatibility with parts
453-
id_parent: str
454-
"""<parent ID>, or <self ID> if not a part."""
455-
id_complete: str
456-
"""<parent ID>.<self ID>, or <self ID> if not a part."""
442+
parts: Mapping[str, NeedsPartType]
457443

458444
# content creation information
459445
jinja_content: bool
@@ -463,6 +449,8 @@ class NeedsInfoType(TypedDict):
463449
content: str
464450
pre_content: None | str
465451
post_content: None | str
452+
doctype: str
453+
"""Type of the document where the need is defined, e.g. '.rst'."""
466454

467455
# these default to False and are updated in check_links post-process
468456
has_dead_links: bool
@@ -473,27 +461,45 @@ class NeedsInfoType(TypedDict):
473461
"""
474462

475463
# constraints information
476-
constraints: list[str]
477-
"""List of constraint names, which are defined for this need."""
478464
# set in process_need_nodes (-> process_constraints) transform
479-
constraints_results: dict[str, dict[str, bool]]
465+
constraints_results: Mapping[str, dict[str, bool]]
480466
"""Mapping of constraint name, to check name, to result."""
481-
constraints_passed: bool
482-
"""True if all constraints passed, False if any failed, None if not yet checked."""
483467
constraints_error: None | str
484468
"""An error message set if any constraint failed, and `error_message` field is set in config."""
485469

486470
# additional source information
487-
doctype: str
488-
"""Type of the document where the need is defined, e.g. '.rst'."""
489471
# set in analyse_need_locations transform
490-
sections: list[str]
491-
section_name: None | str
492-
"""Simply the first section."""
472+
sections: tuple[str, ...]
493473
signature: None | str
494474
"""Derived from a docutils desc_name node."""
475+
476+
arch: Mapping[str, str] # short for architecture.
477+
"""Mapping of uml key to uml content."""
478+
479+
480+
class NeedsInfoComputedType(TypedDict):
481+
"""Data for a single need, that can be computed from existing data.
482+
483+
These fields are used for convenience in filters.
484+
"""
485+
486+
is_need: bool
487+
is_part: bool
488+
is_modified: bool
489+
"""Whether the need was modified by needextend."""
490+
# additional information required for compatibility with parts
491+
id_parent: str
492+
"""<parent ID>, or <self ID> if not a part."""
493+
id_complete: str
494+
"""<parent ID>.<self ID>, or <self ID> if not a part."""
495+
section_name: None | str
496+
"""Simply the first section."""
495497
parent_need: None | str
496498
"""Simply the first parent id."""
499+
constraints: tuple[str, ...]
500+
"""List of constraint names, which are defined for this need."""
501+
constraints_passed: bool
502+
"""True if all constraints passed, False if any failed, None if not yet checked."""
497503

498504

499505
class NeedsBaseDataType(TypedDict):

sphinx_needs/directives/need.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ def analyse_need_locations(app: Sphinx, doctree: nodes.document) -> None:
319319

320320
# first we initialize to default values
321321
if "sections" not in need_info:
322-
need_info["sections"] = []
322+
need_info["sections"] = ()
323323

324324
if "section_name" not in need_info:
325325
need_info["section_name"] = None
@@ -341,15 +341,13 @@ def analyse_need_locations(app: Sphinx, doctree: nodes.document) -> None:
341341

342342
# append / set values from need
343343
if sections:
344-
need_info["sections"] = sections
345-
need_info["section_name"] = sections[0]
344+
need_info["sections"] = tuple(sections)
346345

347346
if signature:
348347
need_info["signature"] = str(signature)
349348

350349
if parent_needs:
351350
need_info["parent_needs"] = parent_needs
352-
need_info["parent_need"] = parent_needs[0]
353351

354352
if need_node.get("hidden"):
355353
hidden_needs.append(need_node)

sphinx_needs/directives/needextend.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ def extend_needs_data(
214214
for found_need in found_needs:
215215
# Work in the stored needs, not on the search result
216216
need = all_needs[found_need["id"]]
217-
need["is_modified"] = True
218217
need["modifications"] += 1
219218

220219
location = (

sphinx_needs/need_constraints.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,8 @@ def process_constraints(needs: NeedsMutable, config: NeedsSphinxConfig) -> None:
2525

2626
for need in needs.values():
2727
need_id = need["id"]
28-
constraints = need["constraints"]
2928

30-
# flag that is set to False if any check fails
31-
need["constraints_passed"] = True
32-
33-
for constraint in constraints:
29+
for constraint in need["constraints_results"]:
3430
try:
3531
executable_constraints = config_constraints[constraint]
3632
except KeyError:
@@ -46,12 +42,8 @@ def process_constraints(needs: NeedsMutable, config: NeedsSphinxConfig) -> None:
4642
# compile constraint and check if need fulfils it
4743
constraint_passed = filter_single_need(need, config, cmd)
4844

49-
if constraint_passed:
50-
need["constraints_results"].setdefault(constraint, {})[name] = True
51-
else:
52-
need["constraints_results"].setdefault(constraint, {})[name] = False
53-
need["constraints_passed"] = False
54-
45+
need["constraints_results"][constraint][name] = constraint_passed
46+
if not constraint_passed:
5547
if "error_message" in executable_constraints:
5648
msg = str(executable_constraints["error_message"])
5749
template = error_templates_cache.setdefault(

0 commit comments

Comments
 (0)