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

152 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 kwargs : variadic keyword arguments of function name to list of backend names 

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

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

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

243 the same as the name of the function). 

244 """ 

245 

246 algos: list[str] 

247 generators: list[str] 

248 

249 def _on_setattr(self, key, value): 

250 from .backends import _registered_algorithms, backend_info 

251 

252 if key in {"algos", "generators"}: 

253 pass 

254 elif key not in _registered_algorithms: 

255 raise AttributeError( 

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

257 "of a dispatchable function (e.g. `.name` attribute of the function)." 

258 ) 

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

260 raise TypeError( 

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

262 ) 

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

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

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

266 return value 

267 

268 def _on_delattr(self, key): 

269 if key in {"algos", "generators"}: 

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

271 

272 

273class NetworkXConfig(Config): 

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

275 

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

277 

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

279 True 

280 

281 Parameters 

282 ---------- 

283 backend_priority : list of backend names or dict or BackendPriorities 

284 Enable automatic conversion of graphs to backend graphs for functions 

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

286 This is a nested configuration with keys ``algos``, ``generators``, and, 

287 optionally, function names. Setting this value to a list of backend names 

288 will set ``nx.config.backend_priority.algos``. For more information, see 

289 ``help(nx.config.backend_priority)``. Default is empty list. 

290 

291 backends : Config mapping of backend names to backend Config 

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

293 and the values are their configurations as Config mappings. 

294 

295 cache_converted_graphs : bool 

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

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

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

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

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

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

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

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

304 Default is True. 

305 

306 fallback_to_nx : bool 

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

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

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

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

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

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

313 Default is False. 

314 

315 warnings_to_ignore : set of strings 

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

317 

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

319 

320 Notes 

321 ----- 

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

323 

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

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

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

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

328 

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

330 

331 - ``NETWORKX_BACKEND_PRIORITY_ALGOS``: same as ``NETWORKX_BACKEND_PRIORITY`` 

332 to set ``backend_priority.algos``. 

333 

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

335 """ 

336 

337 backend_priority: BackendPriorities 

338 backends: Config 

339 cache_converted_graphs: bool 

340 fallback_to_nx: bool 

341 warnings_to_ignore: set[str] 

342 

343 def _on_setattr(self, key, value): 

344 from .backends import backend_info 

345 

346 if key == "backend_priority": 

347 if isinstance(value, list): 

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

349 value = BackendPriorities( 

350 **dict( 

351 self.backend_priority, 

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

353 ) 

354 ) 

355 elif isinstance(value, dict): 

356 kwargs = value 

357 value = BackendPriorities(algos=[], generators=[]) 

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

359 setattr(value, key, val) 

360 elif not isinstance(value, BackendPriorities): 

361 raise TypeError( 

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

363 ) 

364 elif key == "backends": 

365 if not ( 

366 isinstance(value, Config) 

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

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

369 ): 

370 raise TypeError( 

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

372 ) 

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

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

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

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

377 if not isinstance(value, bool): 

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

379 elif key == "warnings_to_ignore": 

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

381 raise TypeError( 

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

383 ) 

384 known_warnings = {"cache"} 

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

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

387 raise ValueError( 

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

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

390 ) 

391 return value