@@ -96,11 +96,14 @@ class WrappedSparse:
9696__all__ = ["_dispatch" ]
9797
9898
99- def _get_plugins ():
99+ def _get_plugins (group , * , load_and_call = False ):
100100 if sys .version_info < (3 , 10 ):
101- items = entry_points ()["networkx.plugins" ]
101+ eps = entry_points ()
102+ if group not in eps :
103+ return {}
104+ items = eps [group ]
102105 else :
103- items = entry_points (group = "networkx.plugins" )
106+ items = entry_points (group = group )
104107 rv = {}
105108 for ep in items :
106109 if ep .name in rv :
@@ -109,14 +112,24 @@ def _get_plugins():
109112 RuntimeWarning ,
110113 stacklevel = 2 ,
111114 )
115+ elif load_and_call :
116+ try :
117+ rv [ep .name ] = ep .load ()()
118+ except Exception as exc :
119+ warnings .warn (
120+ f"Error encountered when loading info for plugin { ep .name } : { exc } " ,
121+ RuntimeWarning ,
122+ stacklevel = 2 ,
123+ )
112124 else :
113125 rv [ep .name ] = ep
114126 # nx-loopback plugin is only available when testing (added in conftest.py)
115- del rv [ "nx-loopback" ]
127+ rv . pop ( "nx-loopback" , None )
116128 return rv
117129
118130
119- plugins = _get_plugins ()
131+ plugins = _get_plugins ("networkx.plugins" )
132+ plugin_info = _get_plugins ("networkx.plugin_info" , load_and_call = True )
120133_registered_algorithms = {}
121134
122135
@@ -234,7 +247,7 @@ def __new__(
234247 # standard function-wrapping stuff
235248 # __annotations__ not used
236249 self .__name__ = func .__name__
237- self .__doc__ = func .__doc__
250+ # self.__doc__ = func.__doc__ # __doc__ handled as cached property
238251 self .__defaults__ = func .__defaults__
239252 # We "magically" add `backend=` keyword argument to allow backend to be specified
240253 if func .__kwdefaults__ :
@@ -246,6 +259,10 @@ def __new__(
246259 self .__dict__ .update (func .__dict__ )
247260 self .__wrapped__ = func
248261
262+ # Supplement docstring with backend info; compute and cache when needed
263+ self ._orig_doc = func .__doc__
264+ self ._cached_doc = None
265+
249266 self .orig_func = func
250267 self .name = name
251268 self .edge_attrs = edge_attrs
@@ -306,13 +323,35 @@ def __new__(
306323 # Compute and cache the signature on-demand
307324 self ._sig = None
308325
326+ # Load and cache backends on-demand
327+ self ._backends = {}
328+
329+ # Which backends implement this function?
330+ self .backends = {
331+ backend
332+ for backend , info in plugin_info .items ()
333+ if "functions" in info and name in info ["functions" ]
334+ }
335+
309336 if name in _registered_algorithms :
310337 raise KeyError (
311338 f"Algorithm already exists in dispatch registry: { name } "
312339 ) from None
313340 _registered_algorithms [name ] = self
314341 return self
315342
343+ @property
344+ def __doc__ (self ):
345+ if (rv := self ._cached_doc ) is not None :
346+ return rv
347+ rv = self ._cached_doc = self ._make_doc ()
348+ return rv
349+
350+ @__doc__ .setter
351+ def __doc__ (self , val ):
352+ self ._orig_doc = val
353+ self ._cached_doc = None
354+
316355 @property
317356 def __signature__ (self ):
318357 if self ._sig is None :
@@ -462,7 +501,7 @@ def __call__(self, /, *args, backend=None, **kwargs):
462501 f"{ self .name } () has networkx and { plugin_name } graphs, but NetworkX is not "
463502 f"configured to automatically convert graphs from networkx to { plugin_name } ."
464503 )
465- backend = plugins [ plugin_name ]. load ( )
504+ backend = self . _load_backend ( plugin_name )
466505 if hasattr (backend , self .name ):
467506 if "networkx" in plugin_names :
468507 # We need to convert networkx graphs to backend graphs
@@ -494,9 +533,15 @@ def __call__(self, /, *args, backend=None, **kwargs):
494533 # Default: run with networkx on networkx inputs
495534 return self .orig_func (* args , ** kwargs )
496535
536+ def _load_backend (self , plugin_name ):
537+ if plugin_name in self ._backends :
538+ return self ._backends [plugin_name ]
539+ rv = self ._backends [plugin_name ] = plugins [plugin_name ].load ()
540+ return rv
541+
497542 def _can_backend_run (self , plugin_name , / , * args , ** kwargs ):
498543 """Can the specified backend run this algorithms with these arguments?"""
499- backend = plugins [ plugin_name ]. load ( )
544+ backend = self . _load_backend ( plugin_name )
500545 return hasattr (backend , self .name ) and (
501546 not hasattr (backend , "can_run" ) or backend .can_run (self .name , args , kwargs )
502547 )
@@ -645,7 +690,7 @@ def _convert_arguments(self, plugin_name, args, kwargs):
645690
646691 # It should be safe to assume that we either have networkx graphs or backend graphs.
647692 # Future work: allow conversions between backends.
648- backend = plugins [ plugin_name ]. load ( )
693+ backend = self . _load_backend ( plugin_name )
649694 for gname in self .graphs :
650695 if gname in self .list_graphs :
651696 bound .arguments [gname ] = [
@@ -704,7 +749,7 @@ def _convert_arguments(self, plugin_name, args, kwargs):
704749
705750 def _convert_and_call (self , plugin_name , args , kwargs , * , fallback_to_nx = False ):
706751 """Call this dispatchable function with a backend, converting graphs if necessary."""
707- backend = plugins [ plugin_name ]. load ( )
752+ backend = self . _load_backend ( plugin_name )
708753 if not self ._can_backend_run (plugin_name , * args , ** kwargs ):
709754 if fallback_to_nx :
710755 return self .orig_func (* args , ** kwargs )
@@ -729,7 +774,7 @@ def _convert_and_call_for_tests(
729774 self , plugin_name , args , kwargs , * , fallback_to_nx = False
730775 ):
731776 """Call this dispatchable function with a backend; for use with testing."""
732- backend = plugins [ plugin_name ]. load ( )
777+ backend = self . _load_backend ( plugin_name )
733778 if not self ._can_backend_run (plugin_name , * args , ** kwargs ):
734779 if fallback_to_nx :
735780 return self .orig_func (* args , ** kwargs )
@@ -807,6 +852,49 @@ def _convert_and_call_for_tests(
807852
808853 return backend .convert_to_nx (result , name = self .name )
809854
855+ def _make_doc (self ):
856+ if not self .backends :
857+ return self ._orig_doc
858+ lines = [
859+ "Backends" ,
860+ "--------" ,
861+ ]
862+ for backend in sorted (self .backends ):
863+ info = plugin_info [backend ]
864+ if "short_summary" in info :
865+ lines .append (f"{ backend } : { info ['short_summary' ]} " )
866+ else :
867+ lines .append (backend )
868+ if "functions" not in info or self .name not in info ["functions" ]:
869+ lines .append ("" )
870+ continue
871+
872+ func_info = info ["functions" ][self .name ]
873+ if "extra_docstring" in func_info :
874+ lines .extend (
875+ f" { line } " if line else line
876+ for line in func_info ["extra_docstring" ].split ("\n " )
877+ )
878+ add_gap = True
879+ else :
880+ add_gap = False
881+ if "extra_parameters" in func_info :
882+ if add_gap :
883+ lines .append ("" )
884+ lines .append (" Extra parameters:" )
885+ extra_parameters = func_info ["extra_parameters" ]
886+ for param in sorted (extra_parameters ):
887+ lines .append (f" { param } " )
888+ if desc := extra_parameters [param ]:
889+ lines .append (f" { desc } " )
890+ lines .append ("" )
891+ else :
892+ lines .append ("" )
893+
894+ lines .pop () # Remove last empty line
895+ to_add = "\n " .join (lines )
896+ return f"{ self ._orig_doc .rstrip ()} \n \n { to_add } "
897+
810898 def __reduce__ (self ):
811899 """Allow this object to be serialized with pickle.
812900
0 commit comments