diff --git a/doc/OnlineDocs/_templates/recursive-module.rst b/doc/OnlineDocs/_templates/recursive-module.rst index 90ba5bd2c85..5cdffdd13d8 100644 --- a/doc/OnlineDocs/_templates/recursive-module.rst +++ b/doc/OnlineDocs/_templates/recursive-module.rst @@ -94,7 +94,7 @@ Library Reference :toctree: :template: recursive-module.rst :recursive: -{% for item in modules %} +{% for item in all_modules %} {# Need item != tests for Sphinx >= 8.0; !endswith(.tests) for < 8.0 #} {% if item != 'tests' and not item.endswith('.tests') and item != 'examples' and not item.endswith('.examples') %} diff --git a/pyomo/_archive/__init__.py b/pyomo/_archive/__init__.py index af86df84849..e86c3ba2a66 100644 --- a/pyomo/_archive/__init__.py +++ b/pyomo/_archive/__init__.py @@ -13,6 +13,6 @@ official Pyomo API. These modules are still importable through their old names via -:func:`pyomo.common.moved_module()` +:func:`pyomo.common.deprecation.moved_module()` """ diff --git a/pyomo/common/collections/__init__.py b/pyomo/common/collections/__init__.py index 2b6587bb4f0..9be7e43ba9f 100644 --- a/pyomo/common/collections/__init__.py +++ b/pyomo/common/collections/__init__.py @@ -10,10 +10,10 @@ # ___________________________________________________________________________ -from collections.abc import MutableMapping, MutableSet, Mapping, Set, Sequence -from collections import UserDict +from collections import OrderedDict, UserDict +from collections.abc import Mapping, MutableMapping, MutableSet, Sequence, Set -from .orderedset import OrderedDict, OrderedSet +from .bunch import Bunch from .component_map import ComponentMap, DefaultComponentMap from .component_set import ComponentSet -from .bunch import Bunch +from .orderedset import OrderedSet diff --git a/pyomo/common/collections/_hasher.py b/pyomo/common/collections/_hasher.py new file mode 100644 index 00000000000..737459dae25 --- /dev/null +++ b/pyomo/common/collections/_hasher.py @@ -0,0 +1,72 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2025 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from collections import defaultdict + + +class HashDispatcher(defaultdict): + """Dispatch table for generating "universal" hashing of all Python objects. + + This class manages a dispatch table for providing hash functions for all Python + types. When an object is passed to the Hasher, it determines the appropriate + hashing strategy based on the object's type: + + - If a custom hashing function is registered for the type, it is used. + - If the object is natively hashable, the default hash is used. + - If the object is unhashable, the object's :func:`id()` is used as a fallback. + + The Hasher also includes special handling for tuples by recursively applying the + appropriate hashing strategy to each element within the tuple. + """ + + def __init__(self, *args, **kwargs): + super().__init__(lambda: self._missing_impl, *args, **kwargs) + self[tuple] = self._tuple + + def _missing_impl(self, val): + try: + hash(val) + self[val.__class__] = self._hashable + except: + self[val.__class__] = self._unhashable + return self[val.__class__](val) + + @staticmethod + def _hashable(val): + return val + + @staticmethod + def _unhashable(val): + return id(val) + + def _tuple(self, val): + return tuple(self[i.__class__](i) for i in val) + + def hashable(self, obj, hashable=None): + if isinstance(obj, type): + cls = obj + else: + cls = type(obj) + if hashable is None: + fcn = self.get(cls, None) + if fcn is None: + raise KeyError(obj) + return fcn is self._hashable + self[cls] = self._hashable if hashable else self._unhashable + + +#: The global 'hasher' instance for managing "universal" hashing. +#: +#: This instance of the :class:`HashDispatcher` is used by +#: :class:`~pyomo.common.collections.component_map.ComponentMap` and +#: :class:`~pyomo.common.collections.component_set.ComponentSet` for +#: generating hashes for all Python and Pyomo types. +hasher = HashDispatcher() diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index b0d4e9fb4ac..2d7216274d0 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -9,10 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import collections -from collections.abc import Mapping as collections_Mapping +from collections.abc import Mapping, MutableMapping + from pyomo.common.autoslots import AutoSlots +from ._hasher import hasher + def _rehash_keys(encode, val): if encode: @@ -20,50 +22,10 @@ def _rehash_keys(encode, val): else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()} - - -class _Hasher(collections.defaultdict): - def __init__(self, *args, **kwargs): - super().__init__(lambda: self._missing_impl, *args, **kwargs) - self[tuple] = self._tuple - - def _missing_impl(self, val): - try: - hash(val) - self[val.__class__] = self._hashable - except: - self[val.__class__] = self._unhashable - return self[val.__class__](val) - - @staticmethod - def _hashable(val): - return val - - @staticmethod - def _unhashable(val): - return id(val) - - def _tuple(self, val): - return tuple(self[i.__class__](i) for i in val) - - def hashable(self, obj, hashable=None): - if isinstance(obj, type): - cls = obj - else: - cls = type(obj) - if hashable is None: - fcn = self.get(cls, None) - if fcn is None: - raise KeyError(obj) - return fcn is self._hashable - self[cls] = self._hashable if hashable else self._unhashable - - -_hasher = _Hasher() + return {hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()} -class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping): +class ComponentMap(AutoSlots.Mixin, MutableMapping): """ This class is a replacement for dict that allows Pyomo modeling components to be used as entry keys. The @@ -89,9 +51,9 @@ class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping): """ __slots__ = ("_dict",) - __autoslot_mappers__ = {'_dict': _rehash_keys} + __autoslot_mappers__ = {"_dict": _rehash_keys} # Expose a "public" interface to the global _hasher dict - hasher = _hasher + hasher = hasher def __init__(self, *args, **kwds): # maps id_hash(obj) -> (obj,val) @@ -110,19 +72,19 @@ def __str__(self): def __getitem__(self, obj): try: - return self._dict[_hasher[obj.__class__](obj)][1] + return self._dict[hasher[obj.__class__](obj)][1] except KeyError: - _id = _hasher[obj.__class__](obj) + _id = hasher[obj.__class__](obj) raise KeyError(f"{obj} (key={_id})") from None def __setitem__(self, obj, val): - self._dict[_hasher[obj.__class__](obj)] = (obj, val) + self._dict[hasher[obj.__class__](obj)] = (obj, val) def __delitem__(self, obj): try: - del self._dict[_hasher[obj.__class__](obj)] + del self._dict[hasher[obj.__class__](obj)] except KeyError: - _id = _hasher[obj.__class__](obj) + _id = hasher[obj.__class__](obj) raise KeyError(f"{obj} (key={_id})") from None def __iter__(self): @@ -147,11 +109,11 @@ def update(self, *args, **kwargs): def __eq__(self, other): if self is other: return True - if not isinstance(other, collections_Mapping) or len(self) != len(other): + if not isinstance(other, Mapping) or len(self) != len(other): return False # Note we have already verified the dicts are the same size for key, val in other.items(): - other_id = _hasher[key.__class__](key) + other_id = hasher[key.__class__](key) if other_id not in self._dict: return False self_val = self._dict[other_id][1] @@ -174,20 +136,20 @@ def __ne__(self, other): # def __contains__(self, obj): - return _hasher[obj.__class__](obj) in self._dict + return hasher[obj.__class__](obj) in self._dict def clear(self): - 'D.clear() -> None. Remove all items from D.' + "D.clear() -> None. Remove all items from D." self._dict.clear() def get(self, key, default=None): - 'D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.' + "D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None." if key in self: return self[key] return default def setdefault(self, key, default=None): - 'D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D' + "D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D" if key in self: return self[key] else: @@ -204,7 +166,7 @@ class DefaultComponentMap(ComponentMap): """ - __slots__ = ('default_factory',) + __slots__ = ("default_factory",) def __init__(self, default_factory=None, *args, **kwargs): super().__init__(*args, **kwargs) @@ -217,7 +179,7 @@ def __missing__(self, key): return ans def __getitem__(self, obj): - _key = _hasher[obj.__class__](obj) + _key = hasher[obj.__class__](obj) if _key in self._dict: return self._dict[_key][1] else: diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index 82e40670801..009794a0c80 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -9,11 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableSet as collections_MutableSet -from collections.abc import Set as collections_Set +from collections.abc import MutableSet, Set from pyomo.common.autoslots import AutoSlots -from pyomo.common.collections.component_map import _hasher + +from ._hasher import hasher def _rehash_keys(encode, val): @@ -32,10 +32,10 @@ def _rehash_keys(encode, val): else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {_hasher[obj.__class__](obj): obj for obj in val.values()} + return {hasher[obj.__class__](obj): obj for obj in val.values()} -class ComponentSet(AutoSlots.Mixin, collections_MutableSet): +class ComponentSet(AutoSlots.Mixin, MutableSet): """ This class is a replacement for set that allows Pyomo modeling components to be used as entries. The @@ -60,9 +60,9 @@ class ComponentSet(AutoSlots.Mixin, collections_MutableSet): """ __slots__ = ("_data",) - __autoslot_mappers__ = {'_data': _rehash_keys} + __autoslot_mappers__ = {"_data": _rehash_keys} # Expose a "public" interface to the global _hasher dict - hasher = _hasher + hasher = hasher def __init__(self, iterable=None): # maps id_hash(obj) -> obj @@ -80,14 +80,14 @@ def update(self, iterable): if isinstance(iterable, ComponentSet): self._data.update(iterable._data) else: - self._data.update((_hasher[val.__class__](val), val) for val in iterable) + self._data.update((hasher[val.__class__](val), val) for val in iterable) # # Implement MutableSet abstract methods # def __contains__(self, val): - return _hasher[val.__class__](val) in self._data + return hasher[val.__class__](val) in self._data def __iter__(self): return iter(self._data.values()) @@ -97,11 +97,11 @@ def __len__(self): def add(self, val): """Add an element.""" - self._data[_hasher[val.__class__](val)] = val + self._data[hasher[val.__class__](val)] = val def discard(self, val): """Remove an element. Do not raise an exception if absent.""" - _id = _hasher[val.__class__](val) + _id = hasher[val.__class__](val) if _id in self._data: del self._data[_id] @@ -112,10 +112,10 @@ def discard(self, val): def __eq__(self, other): if self is other: return True - if not isinstance(other, collections_Set): + if not isinstance(other, Set): return False return len(self) == len(other) and all( - _hasher[val.__class__](val) in self._data for val in other + hasher[val.__class__](val) in self._data for val in other ) def __ne__(self, other): @@ -133,7 +133,7 @@ def clear(self): def remove(self, val): """Remove an element. If not a member, raise a KeyError.""" try: - del self._data[_hasher[val.__class__](val)] + del self._data[hasher[val.__class__](val)] except KeyError: - _id = _hasher[val.__class__](val) + _id = hasher[val.__class__](val) raise KeyError(f"{val} (key={_id})") from None diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index c364e016246..8929ae4a65e 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -9,13 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections import OrderedDict from collections.abc import MutableSet + from pyomo.common.autoslots import AutoSlots class OrderedSet(AutoSlots.Mixin, MutableSet): - __slots__ = ('_dict',) + __slots__ = ("_dict",) def __init__(self, iterable=None): # Starting in Python 3.7, dict is ordered (and is faster than @@ -26,7 +26,7 @@ def __init__(self, iterable=None): def __str__(self): """String representation of the mapping.""" - return "OrderedSet(%s)" % (', '.join(repr(x) for x in self)) + return "OrderedSet(%s)" % (", ".join(repr(x) for x in self)) def update(self, iterable): if isinstance(iterable, OrderedSet):