Skip to content

Commit 53bec93

Browse files
authored
Merge pull request #401 from melexis/fix-cache
Fix partial rebuild issues
2 parents a766df7 + bc3bc96 commit 53bec93

3 files changed

Lines changed: 84 additions & 13 deletions

File tree

mlx/traceability/traceability.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ def perform_consistency_check(app, env):
203203
for each item ID that matches it and is not defined as a checklist-item.
204204
"""
205205
env.traceability_collection.process_intermediate_nodes()
206+
# Restore implicit reverse relations on every consistency check to handle incremental builds
207+
if hasattr(env, 'traceability_collection'):
208+
env.traceability_collection.rebuild_implicit_relations()
206209
ItemRelink.remove_placeholders(env.traceability_collection)
207210
try:
208211
env.traceability_collection.self_test(app.config.traceability_notifications.get('undefined-reference'))
@@ -315,8 +318,10 @@ def init_available_relationships(app):
315318
def initialize_environment(app):
316319
"""Perform initializations needed before the build process starts."""
317320
env = app.builder.env
318-
319-
env.traceability_ref_nodes = {}
321+
# Preserve cached environment data across incremental builds
322+
# Only initialize when missing to avoid losing items/refs from unchanged docs
323+
if not hasattr(env, 'traceability_ref_nodes') or env.traceability_ref_nodes is None:
324+
env.traceability_ref_nodes = {}
320325
processed_sort_config = {}
321326
for attr, sort_spec in app.config.traceability_attributes_sort.items():
322327
try:
@@ -325,12 +330,22 @@ def initialize_environment(app):
325330
report_warning(f"Invalid sort configuration for attribute '{attr}': {str(e)}")
326331
# Fall back to default sorting
327332
processed_sort_config[attr] = sorted
328-
env.traceability_collection = TraceableCollection()
333+
if not hasattr(env, 'traceability_collection') or env.traceability_collection is None:
334+
env.traceability_collection = TraceableCollection()
335+
# Always update sort config on the existing collection
329336
env.traceability_collection.attributes_sort = processed_sort_config
330-
# Copy configuration dictionaries to environment to avoid modifying app.config
331-
env.traceability_checklist = dict(app.config.traceability_checklist)
332-
env.traceability_attributes = dict(app.config.traceability_attributes)
333-
env.traceability_attribute_to_string = dict(app.config.traceability_attribute_to_string)
337+
# Copy configuration dictionaries to environment to avoid modifying app.config,
338+
# but preserve any cached data (e.g., checklist query_results) on incremental builds
339+
if not hasattr(env, 'traceability_checklist') or env.traceability_checklist is None:
340+
env.traceability_checklist = dict(app.config.traceability_checklist)
341+
else:
342+
# Update values from config without dropping existing runtime keys
343+
for k, v in app.config.traceability_checklist.items():
344+
env.traceability_checklist.setdefault(k, v)
345+
if not hasattr(env, 'traceability_attributes') or env.traceability_attributes is None:
346+
env.traceability_attributes = dict(app.config.traceability_attributes)
347+
if not hasattr(env, 'traceability_attribute_to_string') or env.traceability_attribute_to_string is None:
348+
env.traceability_attribute_to_string = dict(app.config.traceability_attribute_to_string)
334349

335350
all_relationships = set(app.config.traceability_relationships).union(app.config.traceability_relationships.values())
336351
all_relationships.discard('')
@@ -356,6 +371,17 @@ def initialize_environment(app):
356371
)
357372

358373

374+
def _purge(app, env, docname):
375+
"""Ensure we purge items and relations when a document is updated in incremental builds."""
376+
if hasattr(env, 'traceability_collection'):
377+
env.traceability_collection.remove_items_from_document(docname)
378+
# Purge attribute descriptions defined in this document to avoid stale captions/content
379+
to_delete = [attr_id for attr_id, attr in TraceableItem.defined_attributes.items()
380+
if getattr(attr, 'docname', None) == docname]
381+
for attr_id in to_delete:
382+
del TraceableItem.defined_attributes[attr_id]
383+
384+
359385
# ----------------------------------------------------------------------------
360386
# Event handler helper functions
361387
def add_checklist_attribute(checklist_config, attributes_config, attribute_to_string_config):
@@ -702,6 +728,7 @@ def copy_html_assets(app, exception):
702728
app.add_directive('attribute-sort', AttributeSortDirective)
703729

704730
app.connect('builder-inited', initialize_environment)
731+
app.connect('env-purge-doc', _purge)
705732
app.connect('env-check-consistency', perform_consistency_check)
706733
app.connect('doctree-resolved', process_item_nodes)
707734

mlx/traceability/traceable_base_node.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,24 @@ def make_internal_item_ref(self, app, item_id):
6464
break
6565
item_info = env.traceability_collection.get_item(item_id)
6666
link_item = item_info
67-
notification_item = None
6867
p_node = nodes.paragraph()
6968
p_node['classes'].append('item-link')
7069

71-
# Only create link when target item (or notification item) exists, warn otherwise (in html and terminal)
70+
# Early return if target item is missing
71+
if item_info is None:
72+
p_node.append(nodes.Text(f"{item_id} not defined, broken link"))
73+
return p_node
74+
75+
# If target is a placeholder, try to use the configured notification item; otherwise show broken link
7276
if item_info.is_placeholder:
7377
notification_item_id = app.config.traceability_notifications.get('undefined-reference')
7478
notification_item = env.traceability_collection.get_item(notification_item_id)
75-
if not notification_item:
79+
if notification_item:
80+
link_item = notification_item
81+
else:
7682
self.has_warned_about_undefined(item_info)
77-
txt = nodes.Text('%s not defined, broken link' % item_id)
78-
p_node.append(txt)
83+
p_node.append(nodes.Text(f"{item_id} not defined, broken link"))
7984
return p_node
80-
link_item = notification_item
8185
try:
8286
if self['document'] == link_item.docname and hasattr(app.builder, 'link_suffix'):
8387
# include filename so that the returned node can be reused on every page in the same directory

mlx/traceability/traceable_collection.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,25 @@ def has_item(self, itemid):
114114
'''
115115
return itemid in self.items
116116

117+
def remove_items_from_document(self, docname):
118+
'''Remove all items that originate from the given document.
119+
120+
Args:
121+
docname (str): Document name (without extension) to purge items for
122+
'''
123+
to_remove = [identifier for identifier, item in self.items.items()
124+
if getattr(item, 'docname', None) == docname]
125+
if not to_remove:
126+
return
127+
to_remove_set = set(to_remove)
128+
# Remove the items themselves
129+
for identifier in to_remove:
130+
del self.items[identifier]
131+
# Remove any relations (explicit and implicit) pointing to the removed items from remaining items
132+
for item in self.items.values():
133+
for removed_id in to_remove_set:
134+
item.remove_targets(removed_id, explicit=True, implicit=True)
135+
117136
def add_relation(self, source_id, relation, target_id):
118137
'''
119138
Add relation between two items
@@ -177,6 +196,27 @@ def process_intermediate_nodes(self):
177196
for node in sorted(self._intermediate_nodes, key=attrgetter('order')):
178197
node.apply_effect(self)
179198

199+
def rebuild_implicit_relations(self):
200+
"""Rebuild all implicit (reverse) relations from explicit ones.
201+
202+
This is needed on incremental builds when some documents are re-read and others come from cache,
203+
so implicit reverse links on re-read items are restored based on the existing explicit links.
204+
"""
205+
# Clear all current implicit relations
206+
for item in self.items.values():
207+
item.implicit_relations = {}
208+
209+
# Recreate implicit relations from explicit relations
210+
for source_id, source in self.items.items():
211+
for relation, targets in source.explicit_relations.items():
212+
reverse_relation = self.get_reverse_relation(relation)
213+
if not reverse_relation:
214+
continue
215+
for target_item in (self.items.get(target_id) for target_id in targets):
216+
if not target_item:
217+
continue
218+
target_item.add_target(reverse_relation, source_id, implicit=True)
219+
180220
def export(self, fname):
181221
'''
182222
Exports collection content. The target location of the json file gets created if it doesn't exist yet.

0 commit comments

Comments
 (0)