Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/utils/configs.py: 57%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

153 statements  

1import collections 

2import typing 

3from dataclasses import dataclass 

4 

5__all__ = ["Config"] 

6 

7 

8@dataclass(init=False, eq=False, slots=True, kw_only=True, match_args=False) 

9class Config: 

10 """The base class for NetworkX configuration. 

11 

12 There are two ways to use this to create configurations. The recommended way 

13 is to subclass ``Config`` with docs and annotations. 

14 

15 >>> class MyConfig(Config): 

16 ... '''Breakfast!''' 

17 ... 

18 ... eggs: int 

19 ... spam: int 

20 ... 

21 ... def _on_setattr(self, key, value): 

22 ... assert isinstance(value, int) and value >= 0 

23 ... return value 

24 >>> cfg = MyConfig(eggs=1, spam=5) 

25 

26 Another way is to simply pass the initial configuration as keyword arguments to 

27 the ``Config`` instance: 

28 

29 >>> cfg1 = Config(eggs=1, spam=5) 

30 >>> cfg1 

31 Config(eggs=1, spam=5) 

32 

33 Once defined, config items may be modified, but can't be added or deleted by default. 

34 ``Config`` is a ``Mapping``, and can get and set configs via attributes or brackets: 

35 

36 >>> cfg.eggs = 2 

37 >>> cfg.eggs 

38 2 

39 >>> cfg["spam"] = 42 

40 >>> cfg["spam"] 

41 42 

42 

43 For convenience, it can also set configs within a context with the "with" statement: 

44 

45 >>> with cfg(spam=3): 

46 ... print("spam (in context):", cfg.spam) 

47 spam (in context): 3 

48 >>> print("spam (after context):", cfg.spam) 

49 spam (after context): 42 

50 

51 Subclasses may also define ``_on_setattr`` (as done in the example above) 

52 to ensure the value being assigned is valid: 

53 

54 >>> cfg.spam = -1 

55 Traceback (most recent call last): 

56 ... 

57 AssertionError 

58 

59 If a more flexible configuration object is needed that allows adding and deleting 

60 configurations, then pass ``strict=False`` when defining the subclass: 

61 

62 >>> class FlexibleConfig(Config, strict=False): 

63 ... default_greeting: str = "Hello" 

64 >>> flexcfg = FlexibleConfig() 

65 >>> flexcfg.name = "Mr. Anderson" 

66 >>> flexcfg 

67 FlexibleConfig(default_greeting='Hello', name='Mr. Anderson') 

68 """ 

69 

70 def __init_subclass__(cls, strict=True): 

71 cls._strict = strict 

72 

73 def __new__(cls, **kwargs): 

74 orig_class = cls 

75 if cls is Config: 

76 # Enable the "simple" case of accepting config definition as keywords 

77 cls = type( 

78 cls.__name__, 

79 (cls,), 

80 {"__annotations__": {key: typing.Any for key in kwargs}}, 

81 ) 

82 cls = dataclass( 

83 eq=False, 

84 repr=cls._strict, 

85 slots=cls._strict, 

86 kw_only=True, 

87 match_args=False, 

88 )(cls) 

89 if not cls._strict: 

90 cls.__repr__ = _flexible_repr 

91 cls._orig_class = orig_class # Save original class so we can pickle 

92 cls._prev = None # Stage previous configs to enable use as context manager 

93 cls._context_stack = [] # Stack of previous configs when used as context 

94 instance = object.__new__(cls) 

95 instance.__init__(**kwargs) 

96 return instance 

97 

98 def _on_setattr(self, key, value): 

99 """Process config value and check whether it is valid. Useful for subclasses.""" 

100 return value 

101 

102 def _on_delattr(self, key): 

103 """Callback for when a config item is being deleted. Useful for subclasses.""" 

104 

105 # Control behavior of attributes 

106 def __dir__(self): 

107 return self.__dataclass_fields__.keys() 

108 

109 def __setattr__(self, key, value): 

110 if self._strict and key not in self.__dataclass_fields__: 

111 raise AttributeError(f"Invalid config name: {key!r}") 

112 value = self._on_setattr(key, value) 

113 object.__setattr__(self, key, value) 

114 self.__class__._prev = None 

115 

116 def __delattr__(self, key): 

117 if self._strict: 

118 raise TypeError( 

119 f"Configuration items can't be deleted (can't delete {key!r})." 

120 ) 

121 self._on_delattr(key) 

122 object.__delattr__(self, key) 

123 self.__class__._prev = None 

124 

125 # Be a `collection.abc.Collection` 

126 def __contains__(self, key): 

127 return ( 

128 key in self.__dataclass_fields__ if self._strict else key in self.__dict__ 

129 ) 

130 

131 def __iter__(self): 

132 return iter(self.__dataclass_fields__ if self._strict else self.__dict__) 

133 

134 def __len__(self): 

135 return len(self.__dataclass_fields__ if self._strict else self.__dict__) 

136 

137 def __reversed__(self): 

138 return reversed(self.__dataclass_fields__ if self._strict else self.__dict__) 

139 

140 # Add dunder methods for `collections.abc.Mapping` 

141 def __getitem__(self, key): 

142 try: 

143 return getattr(self, key) 

144 except AttributeError as err: 

145 raise KeyError(*err.args) from None 

146 

147 def __setitem__(self, key, value): 

148 try: 

149 self.__setattr__(key, value) 

150 except AttributeError as err: 

151 raise KeyError(*err.args) from None 

152 

153 def __delitem__(self, key): 

154 try: 

155 self.__delattr__(key) 

156 except AttributeError as err: 

157 raise KeyError(*err.args) from None 

158 

159 _ipython_key_completions_ = __dir__ # config["<TAB> 

160 

161 # Go ahead and make it a `collections.abc.Mapping` 

162 def get(self, key, default=None): 

163 return getattr(self, key, default) 

164 

165 def items(self): 

166 return collections.abc.ItemsView(self) 

167 

168 def keys(self): 

169 return collections.abc.KeysView(self) 

170 

171 def values(self): 

172 return collections.abc.ValuesView(self) 

173 

174 # dataclass can define __eq__ for us, but do it here so it works after pickling 

175 def __eq__(self, other): 

176 if not isinstance(other, Config): 

177 return NotImplemented 

178 return self._orig_class == other._orig_class and self.items() == other.items() 

179 

180 # Make pickle work 

181 def __reduce__(self): 

182 return self._deserialize, (self._orig_class, dict(self)) 

183 

184 @staticmethod 

185 def _deserialize(cls, kwargs): 

186 return cls(**kwargs) 

187 

188 # Allow to be used as context manager 

189 def __call__(self, **kwargs): 

190 kwargs = {key: self._on_setattr(key, val) for key, val in kwargs.items()} 

191 prev = dict(self) 

192 for key, val in kwargs.items(): 

193 setattr(self, key, val) 

194 self.__class__._prev = prev 

195 return self 

196 

197 def __enter__(self): 

198 if self.__class__._prev is None: 

199 raise RuntimeError( 

200 "Config being used as a context manager without config items being set. " 

201 "Set config items via keyword arguments when calling the config object. " 

202 "For example, using config as a context manager should be like:\n\n" 

203 ' >>> with cfg(breakfast="spam"):\n' 

204 " ... ... # Do stuff\n" 

205 ) 

206 self.__class__._context_stack.append(self.__class__._prev) 

207 self.__class__._prev = None 

208 return self 

209 

210 def __exit__(self, exc_type, exc_value, traceback): 

211 prev = self.__class__._context_stack.pop() 

212 for key, val in prev.items(): 

213 setattr(self, key, val) 

214 

215 

216def _flexible_repr(self): 

217 return ( 

218 f"{self.__class__.__qualname__}(" 

219 + ", ".join(f"{key}={val!r}" for key, val in self.__dict__.items()) 

220 + ")" 

221 ) 

222 

223 

224# Register, b/c `Mapping.__subclasshook__` returns `NotImplemented` 

225collections.abc.Mapping.register(Config) 

226 

227 

228class BackendPriorities(Config, strict=False): 

229 """Configuration to control automatic conversion to and calling of backends. 

230 

231 Priority is given to backends listed earlier. 

232 

233 Parameters 

234 ---------- 

235 algos : list of backend names 

236 This controls "algorithms" such as ``nx.pagerank`` that don't return a graph. 

237 generators : list of backend names 

238 This controls "generators" such as ``nx.from_pandas_edgelist`` that return a graph. 

239 classes : list of backend names 

240 This controls graph classes such as ``nx.Graph()``. 

241 kwargs : variadic keyword arguments of function name to list of backend names 

242 This allows each function to be configured separately and will override the config 

243 in ``algos`` or ``generators`` if present. The dispatchable function name may be 

244 gotten from the ``.name`` attribute such as ``nx.pagerank.name`` (it's typically 

245 the same as the name of the function). 

246 """ 

247 

248 algos: list[str] 

249 generators: list[str] 

250 classes: list[str] 

251 

252 def _on_setattr(self, key, value): 

253 from .backends import _registered_algorithms, backend_info 

254 

255 if key in {"algos", "generators", "classes"}: 

256 pass 

257 elif key not in _registered_algorithms: 

258 raise AttributeError( 

259 f"Invalid config name: {key!r}. Expected 'algos', 'generators', " 

260 "'classes', or a name of a dispatchable function " 

261 "(e.g. `.name` attribute of the function)." 

262 ) 

263 if not (isinstance(value, list) and all(isinstance(x, str) for x in value)): 

264 raise TypeError( 

265 f"{key!r} config must be a list of backend names; got {value!r}" 

266 ) 

267 if missing := {x for x in value if x not in backend_info}: 

268 missing = ", ".join(map(repr, sorted(missing))) 

269 raise ValueError(f"Unknown backend when setting {key!r}: {missing}") 

270 return value 

271 

272 def _on_delattr(self, key): 

273 if key in {"algos", "generators", "classes"}: 

274 raise TypeError(f"{key!r} configuration item can't be deleted.") 

275 

276 

277class NetworkXConfig(Config): 

278 """Configuration for NetworkX that controls behaviors such as how to use backends. 

279 

280 Attribute and bracket notation are supported for getting and setting configurations:: 

281 

282 >>> nx.config.backend_priority == nx.config["backend_priority"] 

283 True 

284 

285 Parameters 

286 ---------- 

287 backend_priority : list of backend names or dict or BackendPriorities 

288 Enable automatic conversion of graphs to backend graphs for functions 

289 implemented by the backend. Priority is given to backends listed earlier. 

290 This is a nested configuration with keys ``algos``, ``generators``, 

291 ``classes``, and, optionally, function names. Setting this value to a 

292 list of backend names will set ``nx.config.backend_priority.algos``. 

293 For more information, see ``help(nx.config.backend_priority)``. 

294 Default is empty list. 

295 

296 backends : Config mapping of backend names to backend Config 

297 The keys of the Config mapping are names of all installed NetworkX backends, 

298 and the values are their configurations as Config mappings. 

299 

300 cache_converted_graphs : bool 

301 If True, then save converted graphs to the cache of the input graph. Graph 

302 conversion may occur when automatically using a backend from `backend_priority` 

303 or when using the `backend=` keyword argument to a function call. Caching can 

304 improve performance by avoiding repeated conversions, but it uses more memory. 

305 Care should be taken to not manually mutate a graph that has cached graphs; for 

306 example, ``G[u][v][k] = val`` changes the graph, but does not clear the cache. 

307 Using methods such as ``G.add_edge(u, v, weight=val)`` will clear the cache to 

308 keep it consistent. ``G.__networkx_cache__.clear()`` manually clears the cache. 

309 Default is True. 

310 

311 fallback_to_nx : bool 

312 If True, then "fall back" and run with the default "networkx" implementation 

313 for dispatchable functions not implemented by backends of input graphs. When a 

314 backend graph is passed to a dispatchable function, the default behavior is to 

315 use the implementation from that backend if possible and raise if not. Enabling 

316 ``fallback_to_nx`` makes the networkx implementation the fallback to use instead 

317 of raising, and will convert the backend graph to a networkx-compatible graph. 

318 Default is False. 

319 

320 warnings_to_ignore : set of strings 

321 Control which warnings from NetworkX are not emitted. Valid elements: 

322 

323 - `"cache"`: when a cached value is used from ``G.__networkx_cache__``. 

324 

325 Notes 

326 ----- 

327 Environment variables may be used to control some default configurations: 

328 

329 - ``NETWORKX_BACKEND_PRIORITY``: set ``backend_priority.algos`` from comma-separated names. 

330 - ``NETWORKX_CACHE_CONVERTED_GRAPHS``: set ``cache_converted_graphs`` to True if nonempty. 

331 - ``NETWORKX_FALLBACK_TO_NX``: set ``fallback_to_nx`` to True if nonempty. 

332 - ``NETWORKX_WARNINGS_TO_IGNORE``: set `warnings_to_ignore` from comma-separated names. 

333 

334 and can be used for finer control of ``backend_priority`` such as: 

335 

336 - ``NETWORKX_BACKEND_PRIORITY_ALGOS``: same as ``NETWORKX_BACKEND_PRIORITY`` 

337 to set ``backend_priority.algos``. 

338 

339 This is a global configuration. Use with caution when using from multiple threads. 

340 """ 

341 

342 backend_priority: BackendPriorities 

343 backends: Config 

344 cache_converted_graphs: bool 

345 fallback_to_nx: bool 

346 warnings_to_ignore: set[str] 

347 

348 def _on_setattr(self, key, value): 

349 from .backends import backend_info 

350 

351 if key == "backend_priority": 

352 if isinstance(value, list): 

353 # `config.backend_priority = [backend]` sets `backend_priority.algos` 

354 value = BackendPriorities( 

355 **dict( 

356 self.backend_priority, 

357 algos=self.backend_priority._on_setattr("algos", value), 

358 ) 

359 ) 

360 elif isinstance(value, dict): 

361 kwargs = value 

362 value = BackendPriorities(algos=[], generators=[], classes=[]) 

363 for key, val in kwargs.items(): 

364 setattr(value, key, val) 

365 elif not isinstance(value, BackendPriorities): 

366 raise TypeError( 

367 f"{key!r} config must be a dict of lists of backend names; got {value!r}" 

368 ) 

369 elif key == "backends": 

370 if not ( 

371 isinstance(value, Config) 

372 and all(isinstance(key, str) for key in value) 

373 and all(isinstance(val, Config) for val in value.values()) 

374 ): 

375 raise TypeError( 

376 f"{key!r} config must be a Config of backend configs; got {value!r}" 

377 ) 

378 if missing := {x for x in value if x not in backend_info}: 

379 missing = ", ".join(map(repr, sorted(missing))) 

380 raise ValueError(f"Unknown backend when setting {key!r}: {missing}") 

381 elif key in {"cache_converted_graphs", "fallback_to_nx"}: 

382 if not isinstance(value, bool): 

383 raise TypeError(f"{key!r} config must be True or False; got {value!r}") 

384 elif key == "warnings_to_ignore": 

385 if not (isinstance(value, set) and all(isinstance(x, str) for x in value)): 

386 raise TypeError( 

387 f"{key!r} config must be a set of warning names; got {value!r}" 

388 ) 

389 known_warnings = {"cache"} 

390 if missing := {x for x in value if x not in known_warnings}: 

391 missing = ", ".join(map(repr, sorted(missing))) 

392 raise ValueError( 

393 f"Unknown warning when setting {key!r}: {missing}. Valid entries: " 

394 + ", ".join(sorted(known_warnings)) 

395 ) 

396 return value