Skip to content

Commit 04b8a5f

Browse files
authored
Merge pull request #3708 from eminyouskn/ye/collections
Clean pyomo collections module
2 parents 3e12d96 + ce512b6 commit 04b8a5f

File tree

7 files changed

+117
-83
lines changed

7 files changed

+117
-83
lines changed

doc/OnlineDocs/_templates/recursive-module.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Library Reference
9494
:toctree:
9595
:template: recursive-module.rst
9696
:recursive:
97-
{% for item in modules %}
97+
{% for item in all_modules %}
9898
{# Need item != tests for Sphinx >= 8.0; !endswith(.tests) for < 8.0 #}
9999
{% if item != 'tests' and not item.endswith('.tests')
100100
and item != 'examples' and not item.endswith('.examples') %}

pyomo/_archive/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
official Pyomo API.
1414
1515
These modules are still importable through their old names via
16-
:func:`pyomo.common.moved_module()`
16+
:func:`pyomo.common.deprecation.moved_module()`
1717
1818
"""

pyomo/common/collections/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
# ___________________________________________________________________________
1111

1212

13-
from collections.abc import MutableMapping, MutableSet, Mapping, Set, Sequence
14-
from collections import UserDict
13+
from collections import OrderedDict, UserDict
14+
from collections.abc import Mapping, MutableMapping, MutableSet, Sequence, Set
1515

16-
from .orderedset import OrderedDict, OrderedSet
16+
from .bunch import Bunch
1717
from .component_map import ComponentMap, DefaultComponentMap
1818
from .component_set import ComponentSet
19-
from .bunch import Bunch
19+
from .orderedset import OrderedSet

pyomo/common/collections/_hasher.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2025
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
from collections import defaultdict
13+
14+
15+
class HashDispatcher(defaultdict):
16+
"""Dispatch table for generating "universal" hashing of all Python objects.
17+
18+
This class manages a dispatch table for providing hash functions for all Python
19+
types. When an object is passed to the Hasher, it determines the appropriate
20+
hashing strategy based on the object's type:
21+
22+
- If a custom hashing function is registered for the type, it is used.
23+
- If the object is natively hashable, the default hash is used.
24+
- If the object is unhashable, the object's :func:`id()` is used as a fallback.
25+
26+
The Hasher also includes special handling for tuples by recursively applying the
27+
appropriate hashing strategy to each element within the tuple.
28+
"""
29+
30+
def __init__(self, *args, **kwargs):
31+
super().__init__(lambda: self._missing_impl, *args, **kwargs)
32+
self[tuple] = self._tuple
33+
34+
def _missing_impl(self, val):
35+
try:
36+
hash(val)
37+
self[val.__class__] = self._hashable
38+
except:
39+
self[val.__class__] = self._unhashable
40+
return self[val.__class__](val)
41+
42+
@staticmethod
43+
def _hashable(val):
44+
return val
45+
46+
@staticmethod
47+
def _unhashable(val):
48+
return id(val)
49+
50+
def _tuple(self, val):
51+
return tuple(self[i.__class__](i) for i in val)
52+
53+
def hashable(self, obj, hashable=None):
54+
if isinstance(obj, type):
55+
cls = obj
56+
else:
57+
cls = type(obj)
58+
if hashable is None:
59+
fcn = self.get(cls, None)
60+
if fcn is None:
61+
raise KeyError(obj)
62+
return fcn is self._hashable
63+
self[cls] = self._hashable if hashable else self._unhashable
64+
65+
66+
#: The global 'hasher' instance for managing "universal" hashing.
67+
#:
68+
#: This instance of the :class:`HashDispatcher` is used by
69+
#: :class:`~pyomo.common.collections.component_map.ComponentMap` and
70+
#: :class:`~pyomo.common.collections.component_set.ComponentSet` for
71+
#: generating hashes for all Python and Pyomo types.
72+
hasher = HashDispatcher()

pyomo/common/collections/component_map.py

Lines changed: 21 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,61 +9,23 @@
99
# This software is distributed under the 3-clause BSD License.
1010
# ___________________________________________________________________________
1111

12-
import collections
13-
from collections.abc import Mapping as collections_Mapping
12+
from collections.abc import Mapping, MutableMapping
13+
1414
from pyomo.common.autoslots import AutoSlots
1515

16+
from ._hasher import hasher
17+
1618

1719
def _rehash_keys(encode, val):
1820
if encode:
1921
return val
2022
else:
2123
# object id() may have changed after unpickling,
2224
# so we rebuild the dictionary keys
23-
return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()}
24-
25-
26-
class _Hasher(collections.defaultdict):
27-
def __init__(self, *args, **kwargs):
28-
super().__init__(lambda: self._missing_impl, *args, **kwargs)
29-
self[tuple] = self._tuple
30-
31-
def _missing_impl(self, val):
32-
try:
33-
hash(val)
34-
self[val.__class__] = self._hashable
35-
except:
36-
self[val.__class__] = self._unhashable
37-
return self[val.__class__](val)
38-
39-
@staticmethod
40-
def _hashable(val):
41-
return val
42-
43-
@staticmethod
44-
def _unhashable(val):
45-
return id(val)
46-
47-
def _tuple(self, val):
48-
return tuple(self[i.__class__](i) for i in val)
49-
50-
def hashable(self, obj, hashable=None):
51-
if isinstance(obj, type):
52-
cls = obj
53-
else:
54-
cls = type(obj)
55-
if hashable is None:
56-
fcn = self.get(cls, None)
57-
if fcn is None:
58-
raise KeyError(obj)
59-
return fcn is self._hashable
60-
self[cls] = self._hashable if hashable else self._unhashable
61-
62-
63-
_hasher = _Hasher()
25+
return {hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()}
6426

6527

66-
class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping):
28+
class ComponentMap(AutoSlots.Mixin, MutableMapping):
6729
"""
6830
This class is a replacement for dict that allows Pyomo
6931
modeling components to be used as entry keys. The
@@ -89,9 +51,9 @@ class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping):
8951
"""
9052

9153
__slots__ = ("_dict",)
92-
__autoslot_mappers__ = {'_dict': _rehash_keys}
54+
__autoslot_mappers__ = {"_dict": _rehash_keys}
9355
# Expose a "public" interface to the global _hasher dict
94-
hasher = _hasher
56+
hasher = hasher
9557

9658
def __init__(self, *args, **kwds):
9759
# maps id_hash(obj) -> (obj,val)
@@ -110,19 +72,19 @@ def __str__(self):
11072

11173
def __getitem__(self, obj):
11274
try:
113-
return self._dict[_hasher[obj.__class__](obj)][1]
75+
return self._dict[hasher[obj.__class__](obj)][1]
11476
except KeyError:
115-
_id = _hasher[obj.__class__](obj)
77+
_id = hasher[obj.__class__](obj)
11678
raise KeyError(f"{obj} (key={_id})") from None
11779

11880
def __setitem__(self, obj, val):
119-
self._dict[_hasher[obj.__class__](obj)] = (obj, val)
81+
self._dict[hasher[obj.__class__](obj)] = (obj, val)
12082

12183
def __delitem__(self, obj):
12284
try:
123-
del self._dict[_hasher[obj.__class__](obj)]
85+
del self._dict[hasher[obj.__class__](obj)]
12486
except KeyError:
125-
_id = _hasher[obj.__class__](obj)
87+
_id = hasher[obj.__class__](obj)
12688
raise KeyError(f"{obj} (key={_id})") from None
12789

12890
def __iter__(self):
@@ -147,11 +109,11 @@ def update(self, *args, **kwargs):
147109
def __eq__(self, other):
148110
if self is other:
149111
return True
150-
if not isinstance(other, collections_Mapping) or len(self) != len(other):
112+
if not isinstance(other, Mapping) or len(self) != len(other):
151113
return False
152114
# Note we have already verified the dicts are the same size
153115
for key, val in other.items():
154-
other_id = _hasher[key.__class__](key)
116+
other_id = hasher[key.__class__](key)
155117
if other_id not in self._dict:
156118
return False
157119
self_val = self._dict[other_id][1]
@@ -174,20 +136,20 @@ def __ne__(self, other):
174136
#
175137

176138
def __contains__(self, obj):
177-
return _hasher[obj.__class__](obj) in self._dict
139+
return hasher[obj.__class__](obj) in self._dict
178140

179141
def clear(self):
180-
'D.clear() -> None. Remove all items from D.'
142+
"D.clear() -> None. Remove all items from D."
181143
self._dict.clear()
182144

183145
def get(self, key, default=None):
184-
'D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.'
146+
"D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."
185147
if key in self:
186148
return self[key]
187149
return default
188150

189151
def setdefault(self, key, default=None):
190-
'D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D'
152+
"D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D"
191153
if key in self:
192154
return self[key]
193155
else:
@@ -204,7 +166,7 @@ class DefaultComponentMap(ComponentMap):
204166
205167
"""
206168

207-
__slots__ = ('default_factory',)
169+
__slots__ = ("default_factory",)
208170

209171
def __init__(self, default_factory=None, *args, **kwargs):
210172
super().__init__(*args, **kwargs)
@@ -217,7 +179,7 @@ def __missing__(self, key):
217179
return ans
218180

219181
def __getitem__(self, obj):
220-
_key = _hasher[obj.__class__](obj)
182+
_key = hasher[obj.__class__](obj)
221183
if _key in self._dict:
222184
return self._dict[_key][1]
223185
else:

pyomo/common/collections/component_set.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
# This software is distributed under the 3-clause BSD License.
1010
# ___________________________________________________________________________
1111

12-
from collections.abc import MutableSet as collections_MutableSet
13-
from collections.abc import Set as collections_Set
12+
from collections.abc import MutableSet, Set
1413

1514
from pyomo.common.autoslots import AutoSlots
16-
from pyomo.common.collections.component_map import _hasher
15+
16+
from ._hasher import hasher
1717

1818

1919
def _rehash_keys(encode, val):
@@ -32,10 +32,10 @@ def _rehash_keys(encode, val):
3232
else:
3333
# object id() may have changed after unpickling,
3434
# so we rebuild the dictionary keys
35-
return {_hasher[obj.__class__](obj): obj for obj in val.values()}
35+
return {hasher[obj.__class__](obj): obj for obj in val.values()}
3636

3737

38-
class ComponentSet(AutoSlots.Mixin, collections_MutableSet):
38+
class ComponentSet(AutoSlots.Mixin, MutableSet):
3939
"""
4040
This class is a replacement for set that allows Pyomo
4141
modeling components to be used as entries. The
@@ -60,9 +60,9 @@ class ComponentSet(AutoSlots.Mixin, collections_MutableSet):
6060
"""
6161

6262
__slots__ = ("_data",)
63-
__autoslot_mappers__ = {'_data': _rehash_keys}
63+
__autoslot_mappers__ = {"_data": _rehash_keys}
6464
# Expose a "public" interface to the global _hasher dict
65-
hasher = _hasher
65+
hasher = hasher
6666

6767
def __init__(self, iterable=None):
6868
# maps id_hash(obj) -> obj
@@ -80,14 +80,14 @@ def update(self, iterable):
8080
if isinstance(iterable, ComponentSet):
8181
self._data.update(iterable._data)
8282
else:
83-
self._data.update((_hasher[val.__class__](val), val) for val in iterable)
83+
self._data.update((hasher[val.__class__](val), val) for val in iterable)
8484

8585
#
8686
# Implement MutableSet abstract methods
8787
#
8888

8989
def __contains__(self, val):
90-
return _hasher[val.__class__](val) in self._data
90+
return hasher[val.__class__](val) in self._data
9191

9292
def __iter__(self):
9393
return iter(self._data.values())
@@ -97,11 +97,11 @@ def __len__(self):
9797

9898
def add(self, val):
9999
"""Add an element."""
100-
self._data[_hasher[val.__class__](val)] = val
100+
self._data[hasher[val.__class__](val)] = val
101101

102102
def discard(self, val):
103103
"""Remove an element. Do not raise an exception if absent."""
104-
_id = _hasher[val.__class__](val)
104+
_id = hasher[val.__class__](val)
105105
if _id in self._data:
106106
del self._data[_id]
107107

@@ -112,10 +112,10 @@ def discard(self, val):
112112
def __eq__(self, other):
113113
if self is other:
114114
return True
115-
if not isinstance(other, collections_Set):
115+
if not isinstance(other, Set):
116116
return False
117117
return len(self) == len(other) and all(
118-
_hasher[val.__class__](val) in self._data for val in other
118+
hasher[val.__class__](val) in self._data for val in other
119119
)
120120

121121
def __ne__(self, other):
@@ -133,7 +133,7 @@ def clear(self):
133133
def remove(self, val):
134134
"""Remove an element. If not a member, raise a KeyError."""
135135
try:
136-
del self._data[_hasher[val.__class__](val)]
136+
del self._data[hasher[val.__class__](val)]
137137
except KeyError:
138-
_id = _hasher[val.__class__](val)
138+
_id = hasher[val.__class__](val)
139139
raise KeyError(f"{val} (key={_id})") from None

pyomo/common/collections/orderedset.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
# This software is distributed under the 3-clause BSD License.
1010
# ___________________________________________________________________________
1111

12-
from collections import OrderedDict
1312
from collections.abc import MutableSet
13+
1414
from pyomo.common.autoslots import AutoSlots
1515

1616

1717
class OrderedSet(AutoSlots.Mixin, MutableSet):
18-
__slots__ = ('_dict',)
18+
__slots__ = ("_dict",)
1919

2020
def __init__(self, iterable=None):
2121
# Starting in Python 3.7, dict is ordered (and is faster than
@@ -26,7 +26,7 @@ def __init__(self, iterable=None):
2626

2727
def __str__(self):
2828
"""String representation of the mapping."""
29-
return "OrderedSet(%s)" % (', '.join(repr(x) for x in self))
29+
return "OrderedSet(%s)" % (", ".join(repr(x) for x in self))
3030

3131
def update(self, iterable):
3232
if isinstance(iterable, OrderedSet):

0 commit comments

Comments
 (0)