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