Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/OnlineDocs/_templates/recursive-module.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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') %}
Expand Down
2 changes: 1 addition & 1 deletion pyomo/_archive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()`

"""
8 changes: 4 additions & 4 deletions pyomo/common/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
72 changes: 72 additions & 0 deletions pyomo/common/collections/_hasher.py
Original file line number Diff line number Diff line change
@@ -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()
80 changes: 21 additions & 59 deletions pyomo/common/collections/component_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,23 @@
# 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:
return 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
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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]
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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:
Expand Down
30 changes: 15 additions & 15 deletions pyomo/common/collections/component_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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())
Expand All @@ -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]

Expand All @@ -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):
Expand All @@ -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
6 changes: 3 additions & 3 deletions pyomo/common/collections/orderedset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
Loading