4848
4949 The ``issue-tracker`` attribute can be used to specify the project's issue tracking URL
5050
51+ TODO: document that fields can be configured in project
52+
5153 *Since: 2.5*
5254
5355
370372"""
371373
372374import os
375+ from collections import ChainMap
373376from contextlib import contextmanager
374377from typing import Iterable , Iterator , Optional , Tuple , Dict , Any , Set , TYPE_CHECKING , Union
375378from dataclasses import dataclass
378381from .node import MappingNode
379382from .plugin import Plugin
380383from .sourcemirror import SourceMirror
381- from .types import SourceRef , CoreWarnings , FastEnum , _SourceProvenance
382- from ._exceptions import BstError , ImplError , PluginError
383- from .exceptions import ErrorDomain
384+ from .types import SourceRef , CoreWarnings , FastEnum
385+ from ._exceptions import BstError , ImplError , PluginError , LoadError
386+ from .exceptions import ErrorDomain , LoadErrorReason
384387from ._loader .metasource import MetaSource
385388from ._projectrefs import ProjectRefStorage
386389from ._cachekey import generate_key
396399
397400 # pylint: enable=cyclic-import
398401
402+ SourceProvenance = MappingNode
399403
400404class SourceError (BstError ):
401405 """This exception should be raised by :class:`.Source` implementations
@@ -553,8 +557,7 @@ def __init__(
553557 self ,
554558 kind : str ,
555559 url : str ,
556- homepage : Optional [str ],
557- issue_tracker : Optional [str ],
560+ provenance : Optional [MappingNode ],
558561 medium : Union [SourceInfoMedium , str ],
559562 version_type : Union [SourceVersionType , str ],
560563 version : str ,
@@ -572,14 +575,9 @@ def __init__(
572575 The url of the source input
573576 """
574577
575- self .homepage : Optional [str ] = homepage
576- """
577- The project homepage URL
578- """
579-
580- self .issue_tracker : Optional [str ] = issue_tracker
578+ self .provenance = provenance
581579 """
582- The project issue tracking URL
580+ The optional YAML node with source provenance attributes
583581 """
584582
585583 self .medium : Union [SourceInfoMedium , str ] = medium
@@ -642,10 +640,14 @@ def serialize(self) -> Dict[str, Union[str, Dict[str, str]]]:
642640 "url" : self .url ,
643641 }
644642
645- if self .homepage is not None :
646- version_info ["homepage" ] = self .homepage
647- if self .issue_tracker is not None :
648- version_info ["issue-tracker" ] = self .issue_tracker
643+ if self .provenance is not None :
644+ # need to keep homepage/issue-tracker [also] at the top-level for backward compat
645+ if (homepage := self .provenance .get_str ("homepage" , None )) is not None :
646+ version_info ["homepage" ] = homepage
647+ if (issue_tracker := self .provenance .get_str ("issue-tracker" , None )) is not None :
648+ version_info ["issue-tracker" ] = issue_tracker
649+
650+ version_info ["provenance" ] = self .provenance
649651
650652 version_info ["medium" ] = medium_str
651653 version_info ["version-type" ] = version_type_str
@@ -825,7 +827,7 @@ def __init__(
825827 self ._directory = meta .directory # Staging relative directory
826828 self .__variables = variables # The variables used to resolve the source's config
827829 self .__provenance : Optional [
828- _SourceProvenance
830+ MappingNode
829831 ] = meta .provenance # The _SourceProvenance for general user provided SourceInfo
830832
831833 self .__key = None # Cache key for source
@@ -927,7 +929,16 @@ def set_ref(self, ref, node):
927929 """
928930 raise ImplError ("Source plugin '{}' does not implement set_ref()" .format (self .get_kind ()))
929931
930- def track (self , * , previous_sources_dir : Optional [str ] = None ) -> SourceRef :
932+ def load_source_provenance (self , node : MappingNode ) -> None :
933+ raise ImplError ("Source plugin '{}' does not implement load_source_provenance()" .format (self .get_kind ()))
934+
935+ def get_source_provenance (self ) -> SourceProvenance :
936+ raise ImplError ("Source plugin '{}' does not implement get_source_provenance()" .format (self .get_kind ()))
937+
938+ def set_source_provenance (self , source_provenance : SourceProvenance ):
939+ raise ImplError ("Source plugin '{}' does not implement set_source_provenance()" .format (self .get_kind ()))
940+
941+ def track (self , * , previous_sources_dir : Optional [str ] = None ) -> (SourceRef , SourceProvenance ):
931942 """Resolve a new ref from the plugin's track option
932943
933944 Args:
@@ -1393,23 +1404,24 @@ def create_source_info(
13931404
13941405 *Since: 2.5*
13951406 """
1396- homepage = None
1397- issue_tracker = None
1407+ project = self ._get_project ()
13981408
13991409 if provenance_node is not None :
1400- provenance : Optional [_SourceProvenance ] = _SourceProvenance .new_from_node (provenance_node )
1410+ # TODO: Ensure provenance node keys are valid and values are all strings; same in element if we drop _SourceProvenance
1411+ try :
1412+ provenance_node .validate_keys (project .source_provenance_fields .keys ())
1413+ except LoadError as E :
1414+ raise LoadError ("Specified source attribute not defined in project config\n {}" .format (E ),
1415+ LoadErrorReason .UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE )
1416+
1417+ provenance = provenance_node
14011418 else :
14021419 provenance = self .__provenance
14031420
1404- if provenance is not None :
1405- homepage = provenance .homepage
1406- issue_tracker = provenance .issue_tracker
1407-
14081421 return SourceInfo (
14091422 self .get_kind (),
14101423 url ,
1411- homepage ,
1412- issue_tracker ,
1424+ provenance ,
14131425 medium ,
14141426 version_type ,
14151427 version ,
@@ -1778,13 +1790,30 @@ def process_value(action, container, path, key, new_value):
17781790 # previous_sources_dir (str): directory where previous sources are staged
17791791 #
17801792 def _track (self , previous_sources_dir : Optional [str ] = None ) -> SourceRef :
1793+ def verify_provenance_attributes (provenance : SourceProvenance ):
1794+ if type (provenance ) is list :
1795+ # produce a list of unique attrs
1796+ unique_entries = {attr : "" for single_provenance in provenance for attr in single_provenance .keys ()}
1797+ used_attrs = unique_entries .keys ()
1798+ else :
1799+ used_attrs = provenance .keys ()
1800+
1801+ undefined_attributes = list (set (used_attrs ) - set (self ._get_project ().source_provenance_fields .keys ()))
1802+
1803+ if len (undefined_attributes ) > 0 :
1804+ self .warn (f"Required source attributes not defined in project config: { undefined_attributes } " )
1805+
17811806 if self .BST_REQUIRES_PREVIOUS_SOURCES_TRACK :
1782- new_ref = self .__do_track (previous_sources_dir = previous_sources_dir )
1807+ new_ref , source_provenance = self .__do_track (previous_sources_dir = previous_sources_dir )
17831808 else :
1784- new_ref = self .__do_track ()
1809+ new_ref , source_provenance = self .__do_track ()
17851810
17861811 current_ref = self .get_ref () # pylint: disable=assignment-from-no-return
17871812
1813+ # in the case of multi-source plugins, set_ref might update the provenance with the new content,
1814+ # designed to use ref, so grab the old content first
1815+ current_provenance : list [MappingNode ] = self .get_source_provenance ()
1816+
17881817 if new_ref is None :
17891818 # No tracking, keep current ref
17901819 new_ref = current_ref
@@ -1797,6 +1826,18 @@ def _track(self, previous_sources_dir: Optional[str] = None) -> SourceRef:
17971826
17981827 self ._generate_key ()
17991828
1829+ # need to account for multi-source provenance and process it appropriately
1830+ if source_provenance is not None :
1831+ verify_provenance_attributes (source_provenance )
1832+
1833+ if current_provenance is not None :
1834+ current_provenance = list (map (MappingNode .strip_node_info , current_provenance ))
1835+
1836+ if current_provenance != source_provenance :
1837+ self .info (f"Updated source provenance content for { self .name } " )
1838+
1839+ self .set_source_provenance (source_provenance )
1840+
18001841 return new_ref
18011842
18021843 # _requires_previous_sources()
@@ -2025,14 +2066,14 @@ def __do_track(self, **kwargs):
20252066 for mirror in reversed (project .get_alias_uris (alias , first_pass = self .__first_pass , tracking = True )):
20262067 new_source = self .__clone_for_uri (mirror )
20272068 try :
2028- ref = new_source .track (** kwargs ) # pylint: disable=assignment-from-none
2069+ ref , provenance = new_source .track (** kwargs ) # pylint: disable=assignment-from-none
20292070 # FIXME: Need to consider temporary vs. permanent failures,
20302071 # and how this works with retries.
20312072 except BstError as e :
20322073 last_error = e
20332074 continue
20342075
2035- return ref
2076+ return ( ref , provenance )
20362077
20372078 raise last_error
20382079
0 commit comments