Skip to content
Open
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
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Dockerfile for boltons development and testing
# Python 3.10 provides good compatibility with the codebase
FROM python:3.13-slim

# Set working directory
WORKDIR /app

# Copy the entire project into the container
COPY . .

# Install system dependencies (git is useful for development)
RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*

# Upgrade pip to latest version
RUN pip install --upgrade pip

# Install testing dependencies with pinned versions
RUN pip install --no-cache-dir \
pytest==7.4.3 \
pytest-cov==4.1.0

# Install boltons in editable mode
# This allows the tests to import boltons modules correctly
RUN pip install -e .

# Set environment variables for better Python behavior in containers
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

# Default command runs the test suite
CMD ["pytest", "tests/", "-v"]
146 changes: 145 additions & 1 deletion boltons/dictutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
PREV, NEXT, KEY, VALUE, SPREV, SNEXT = range(6)


__all__ = ['MultiDict', 'OMD', 'OrderedMultiDict', 'OneToOne', 'ManyToMany', 'subdict', 'FrozenDict']
__all__ = ['MultiDict', 'OMD', 'OrderedMultiDict', 'OneToOne', 'ManyToMany', 'subdict', 'FrozenDict', 'FrozenOneToOne']


class OrderedMultiDict(dict):
Expand Down Expand Up @@ -1117,4 +1117,148 @@ def _raise_frozen_typeerror(self, *a, **kw):
del _raise_frozen_typeerror


_FOTO_INV_MARKER = object()


class FrozenOneToOne(dict):
"""An immutable one-to-one mapping dictionary that is hashable and can
itself be used as a :class:`dict` key or :class:`set` entry. This is to
:class:`OneToOne` what :class:`FrozenDict` is to :class:`dict`.

Like :class:`OneToOne`, all values are automatically available as keys
on a reverse mapping via the `inv` attribute, maintaining distinct key
and value namespaces.

Basic operations work as expected:

>>> foto = FrozenOneToOne({'a': 1, 'b': 2})
>>> print(foto['a'])
1
>>> print(foto.inv[1])
a
>>> len(foto)
2
>>> hash(foto) # doctest: +SKIP
-1234567890

Because FrozenOneToOne is immutable, all mutating operations raise
:exc:`TypeError`:

>>> foto['c'] = 3 # doctest: +SKIP
Traceback (most recent call last):
...
TypeError: FrozenOneToOne object is immutable

To create a modified version, use the :meth:`updated` method:

>>> foto2 = foto.updated({'c': 3})
>>> foto2['c']
3
>>> 'c' in foto # original unchanged
False
"""
__slots__ = ('inv', '_hash')

def __init__(self, *a, **kw):
if a and a[0] is _FOTO_INV_MARKER:
# Internal constructor for creating the inverse
object.__setattr__(self, 'inv', a[1])
dict.__init__(self, [(v, k) for k, v in self.inv.items()])
return

# Validate that values are unique and hashable
dict.__init__(self, *a, **kw)

# Check for duplicate values
val_multidict = {}
for k, v in self.items():
hash(v) # Ensure value is hashable
val_multidict.setdefault(v, []).append(k)

dupes = {v: k_list for v, k_list in val_multidict.items()
if len(k_list) > 1}

if dupes:
raise ValueError('expected unique values, got multiple keys for'
' the following values: %r' % dupes)

object.__setattr__(self, 'inv', self.__class__(_FOTO_INV_MARKER, self))

def updated(self, *a, **kw):
"""Create a new FrozenOneToOne with updated mappings.

Like :meth:`dict.update`, but returns a new FrozenOneToOne instead
of modifying in place.

Args:
*a: A dict or iterable of key-value pairs
**kw: Keyword arguments for additional key-value pairs

Returns:
A new FrozenOneToOne with the updated mappings
"""
data = dict(self)
if a:
if len(a) > 1:
raise TypeError('updated expected at most 1 argument, got %s' % len(a))
other = a[0]
if hasattr(other, 'items'):
data.update(other)
else:
data.update(dict(other))
data.update(kw)
return type(self)(data)

@classmethod
def fromkeys(cls, keys, value=None):
"""Create a FrozenOneToOne from a sequence of keys with a shared value.

Note: Since values must be unique in a one-to-one mapping, this only
works if there's a single key or if you want duplicate-key behavior.
"""
return cls(dict.fromkeys(keys, value))

def __repr__(self):
cn = self.__class__.__name__
dict_repr = dict.__repr__(self)
return f"{cn}({dict_repr})"

def __reduce_ex__(self, protocol):
return type(self), (dict(self),)

def __hash__(self):
try:
ret = object.__getattribute__(self, '_hash')
except AttributeError:
try:
ret = hash(frozenset(self.items()))
object.__setattr__(self, '_hash', ret)
except Exception as e:
ret = FrozenHashError(e)
object.__setattr__(self, '_hash', ret)

if ret.__class__ is FrozenHashError:
raise ret

return ret

def __copy__(self):
return self

def __deepcopy__(self, memo):
return self

def copy(self):
"""Return self (FrozenOneToOne is immutable)."""
return self

def _raise_frozen_typeerror(self, *a, **kw):
raise TypeError('%s object is immutable' % self.__class__.__name__)

__ior__ = __setitem__ = __delitem__ = update = _raise_frozen_typeerror
setdefault = pop = popitem = clear = _raise_frozen_typeerror

del _raise_frozen_typeerror


# end dictutils.py
Loading