@@ -96,11 +96,14 @@ class WrappedSparse:
96
96
__all__ = ["_dispatch" ]
97
97
98
98
99
- def _get_plugins ():
99
+ def _get_plugins (group , * , load_and_call = False ):
100
100
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 ]
102
105
else :
103
- items = entry_points (group = "networkx.plugins" )
106
+ items = entry_points (group = group )
104
107
rv = {}
105
108
for ep in items :
106
109
if ep .name in rv :
@@ -109,14 +112,24 @@ def _get_plugins():
109
112
RuntimeWarning ,
110
113
stacklevel = 2 ,
111
114
)
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
+ )
112
124
else :
113
125
rv [ep .name ] = ep
114
126
# nx-loopback plugin is only available when testing (added in conftest.py)
115
- del rv [ "nx-loopback" ]
127
+ rv . pop ( "nx-loopback" , None )
116
128
return rv
117
129
118
130
119
- plugins = _get_plugins ()
131
+ plugins = _get_plugins ("networkx.plugins" )
132
+ plugin_info = _get_plugins ("networkx.plugin_info" , load_and_call = True )
120
133
_registered_algorithms = {}
121
134
122
135
@@ -234,7 +247,7 @@ def __new__(
234
247
# standard function-wrapping stuff
235
248
# __annotations__ not used
236
249
self .__name__ = func .__name__
237
- self .__doc__ = func .__doc__
250
+ # self.__doc__ = func.__doc__ # __doc__ handled as cached property
238
251
self .__defaults__ = func .__defaults__
239
252
# We "magically" add `backend=` keyword argument to allow backend to be specified
240
253
if func .__kwdefaults__ :
@@ -246,6 +259,10 @@ def __new__(
246
259
self .__dict__ .update (func .__dict__ )
247
260
self .__wrapped__ = func
248
261
262
+ # Supplement docstring with backend info; compute and cache when needed
263
+ self ._orig_doc = func .__doc__
264
+ self ._cached_doc = None
265
+
249
266
self .orig_func = func
250
267
self .name = name
251
268
self .edge_attrs = edge_attrs
@@ -306,13 +323,35 @@ def __new__(
306
323
# Compute and cache the signature on-demand
307
324
self ._sig = None
308
325
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
+
309
336
if name in _registered_algorithms :
310
337
raise KeyError (
311
338
f"Algorithm already exists in dispatch registry: { name } "
312
339
) from None
313
340
_registered_algorithms [name ] = self
314
341
return self
315
342
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
+
316
355
@property
317
356
def __signature__ (self ):
318
357
if self ._sig is None :
@@ -462,7 +501,7 @@ def __call__(self, /, *args, backend=None, **kwargs):
462
501
f"{ self .name } () has networkx and { plugin_name } graphs, but NetworkX is not "
463
502
f"configured to automatically convert graphs from networkx to { plugin_name } ."
464
503
)
465
- backend = plugins [ plugin_name ]. load ( )
504
+ backend = self . _load_backend ( plugin_name )
466
505
if hasattr (backend , self .name ):
467
506
if "networkx" in plugin_names :
468
507
# We need to convert networkx graphs to backend graphs
@@ -494,9 +533,15 @@ def __call__(self, /, *args, backend=None, **kwargs):
494
533
# Default: run with networkx on networkx inputs
495
534
return self .orig_func (* args , ** kwargs )
496
535
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
+
497
542
def _can_backend_run (self , plugin_name , / , * args , ** kwargs ):
498
543
"""Can the specified backend run this algorithms with these arguments?"""
499
- backend = plugins [ plugin_name ]. load ( )
544
+ backend = self . _load_backend ( plugin_name )
500
545
return hasattr (backend , self .name ) and (
501
546
not hasattr (backend , "can_run" ) or backend .can_run (self .name , args , kwargs )
502
547
)
@@ -645,7 +690,7 @@ def _convert_arguments(self, plugin_name, args, kwargs):
645
690
646
691
# It should be safe to assume that we either have networkx graphs or backend graphs.
647
692
# Future work: allow conversions between backends.
648
- backend = plugins [ plugin_name ]. load ( )
693
+ backend = self . _load_backend ( plugin_name )
649
694
for gname in self .graphs :
650
695
if gname in self .list_graphs :
651
696
bound .arguments [gname ] = [
@@ -704,7 +749,7 @@ def _convert_arguments(self, plugin_name, args, kwargs):
704
749
705
750
def _convert_and_call (self , plugin_name , args , kwargs , * , fallback_to_nx = False ):
706
751
"""Call this dispatchable function with a backend, converting graphs if necessary."""
707
- backend = plugins [ plugin_name ]. load ( )
752
+ backend = self . _load_backend ( plugin_name )
708
753
if not self ._can_backend_run (plugin_name , * args , ** kwargs ):
709
754
if fallback_to_nx :
710
755
return self .orig_func (* args , ** kwargs )
@@ -729,7 +774,7 @@ def _convert_and_call_for_tests(
729
774
self , plugin_name , args , kwargs , * , fallback_to_nx = False
730
775
):
731
776
"""Call this dispatchable function with a backend; for use with testing."""
732
- backend = plugins [ plugin_name ]. load ( )
777
+ backend = self . _load_backend ( plugin_name )
733
778
if not self ._can_backend_run (plugin_name , * args , ** kwargs ):
734
779
if fallback_to_nx :
735
780
return self .orig_func (* args , ** kwargs )
@@ -807,6 +852,49 @@ def _convert_and_call_for_tests(
807
852
808
853
return backend .convert_to_nx (result , name = self .name )
809
854
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
+
810
898
def __reduce__ (self ):
811
899
"""Allow this object to be serialized with pickle.
812
900
0 commit comments