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
32 changes: 30 additions & 2 deletions Lib/immutable.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import _immutable as _c

register_freezable = _c.register_freezable
freeze = _c.freeze
isfrozen = _c.isfrozen
set_freezable = _c.set_freezable
Expand All @@ -20,8 +19,33 @@
FREEZABLE_EXPLICIT = _c.FREEZABLE_EXPLICIT
FREEZABLE_PROXY = _c.FREEZABLE_PROXY


def freezable(cls):
"""Class decorator: mark a class as always freezable."""
set_freezable(cls, FREEZABLE_YES)
return cls


def unfreezable(cls):
"""Class decorator: mark a class as never freezable."""
set_freezable(cls, FREEZABLE_NO)
return cls


def explicitlyFreezable(cls):
"""Class decorator: mark a class as freezable only when passed directly to freeze()."""
set_freezable(cls, FREEZABLE_EXPLICIT)
return cls


def frozen(cls):
"""Class decorator: make a class freezable, then freeze it."""
set_freezable(cls, FREEZABLE_YES)
freeze(cls)
return cls


__all__ = [
"register_freezable",
"freeze",
"isfrozen",
"set_freezable",
Expand All @@ -32,6 +56,10 @@
"FREEZABLE_NO",
"FREEZABLE_EXPLICIT",
"FREEZABLE_PROXY",
"freezable",
"unfreezable",
"explicitlyFreezable",
"frozen",
]

__version__ = getattr(_c, "__version__", "1.0")
127 changes: 127 additions & 0 deletions Lib/test/test_freeze/test_decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Tests for immutable module decorators."""

import unittest
from immutable import (
freeze, isfrozen, freezable, unfreezable, explicitlyFreezable, frozen,
)


class TestFreezableDecorator(unittest.TestCase):

def test_freezable_class_can_be_frozen(self):
@freezable
class C:
pass
obj = C()
freeze(obj)
self.assertTrue(isfrozen(obj))

def test_freezable_returns_class(self):
@freezable
class C:
pass
self.assertEqual(C.__name__, 'C')


class TestUnfreezableDecorator(unittest.TestCase):

def test_unfreezable_class_raises(self):
@unfreezable
class C:
pass
obj = C()
with self.assertRaises(TypeError):
freeze(obj)
self.assertFalse(isfrozen(obj))

def test_unfreezable_returns_class(self):
@unfreezable
class C:
pass
self.assertEqual(C.__name__, 'C')


class TestExplicitlyFreezableDecorator(unittest.TestCase):

def test_explicit_direct_freeze_succeeds(self):
@explicitlyFreezable
class C:
pass
# The class itself can be frozen when passed directly.
freeze(C)
self.assertTrue(isfrozen(C))

def test_explicit_as_child_fails(self):
@freezable
class Parent:
pass

@explicitlyFreezable
class Child:
pass

p = Parent()
p.child = Child()
# Child's type is EXPLICIT, so freezing parent (which reaches
# Child's type as a child) should fail.
with self.assertRaises(TypeError):
freeze(p)

def test_explicit_returns_class(self):
@explicitlyFreezable
class C:
pass
self.assertEqual(C.__name__, 'C')


class TestFrozenDecorator(unittest.TestCase):

def test_frozen_class_is_frozen(self):
@frozen
class C:
pass
self.assertTrue(isfrozen(C))

def test_frozen_class_is_immutable(self):
@frozen
class C:
pass
with self.assertRaises(TypeError):
C.new_attr = 42

def test_frozen_returns_class(self):
@frozen
class C:
pass
self.assertEqual(C.__name__, 'C')


class TestFreezeReturnValue(unittest.TestCase):
"""freeze() returns its first argument."""

def test_returns_same_object(self):
@freezable
class C:
pass
obj = C()
result = freeze(obj)
self.assertIs(result, obj)

def test_returns_first_of_many(self):
@freezable
class C:
pass
a, b, c = C(), C(), C()
result = freeze(a, b, c)
self.assertIs(result, a)

def test_frozen_decorator_returns_class(self):
@frozen
class C:
pass
self.assertIsInstance(C, type)
self.assertTrue(isfrozen(C))


if __name__ == '__main__':
unittest.main()
8 changes: 4 additions & 4 deletions Lib/test/test_freeze/test_multi_freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import unittest
from immutable import (
freeze, isfrozen, register_freezable, set_freezable,
FREEZABLE_EXPLICIT, FREEZABLE_NO,
freeze, isfrozen, set_freezable,
FREEZABLE_EXPLICIT, FREEZABLE_NO, FREEZABLE_YES,
)


def make_freezable_class():
"""Create a fresh class registered as freezable."""
"""Create a fresh class marked as freezable."""
class C:
pass
register_freezable(C)
set_freezable(C, FREEZABLE_YES)
return C


Expand Down
8 changes: 4 additions & 4 deletions Lib/test/test_freeze/test_rollback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
import unittest
import weakref
from immutable import (
freeze, isfrozen, register_freezable, set_freezable,
FREEZABLE_NO,
freeze, isfrozen, set_freezable,
FREEZABLE_NO, FREEZABLE_YES,
)


def make_freezable_class():
"""Create a fresh class registered as freezable."""
"""Create a fresh class marked as freezable."""
class C:
pass
register_freezable(C)
set_freezable(C, FREEZABLE_YES)
return C


Expand Down
21 changes: 13 additions & 8 deletions Lib/test/test_freeze/test_set_freezable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
import unittest
import weakref
from immutable import (
freeze, isfrozen, register_freezable, set_freezable,
freeze, isfrozen, set_freezable,
FREEZABLE_YES, FREEZABLE_NO, FREEZABLE_EXPLICIT, FREEZABLE_PROXY,
)


def make_freezable_class():
"""Create a fresh class registered as freezable."""
"""Create a fresh class marked as freezable."""
class C:
pass
register_freezable(C)
set_freezable(C, FREEZABLE_YES)
return C


Expand Down Expand Up @@ -170,17 +170,22 @@ def test_attr_storage_updates_on_override(self):
self.assertEqual(obj.__freezable__, FREEZABLE_YES)

def test_ob_flags_fallback_for_slots_only(self):
# Objects with __slots__ but no __dict__ should fall back
# to ob_flags on 64-bit.
# Objects with __slots__ but no __dict__ use ob_flags for
# instance-level set_freezable on 64-bit.
# Use _immutable.register_freezable (the low-level C API) to
# register the type without setting __freezable__, so the
# per-instance ob_flags path is tested in isolation.
import sys
import _immutable
if sys.maxsize <= 2**31:
self.skipTest("ob_flags fallback not available on 32-bit")
class S:
__slots__ = ('__weakref__', 'x')
register_freezable(S)
_immutable.register_freezable(S)
obj = S()
set_freezable(obj, FREEZABLE_NO)
# No __freezable__ attribute should be set.
# No __freezable__ attribute should be set on the instance
# (it has no __dict__).
self.assertFalse(hasattr(obj, '__freezable__'))
# But the status should still be queryable during freeze.
with self.assertRaises(TypeError):
Expand Down Expand Up @@ -214,7 +219,7 @@ def test_ob_flags_path_no_prevent_gc(self):
# set_freezable should not prevent collection.
class S:
__slots__ = ('__weakref__', 'x')
register_freezable(S)
set_freezable(S, FREEZABLE_YES)
obj = S()
ref = weakref.ref(obj)
set_freezable(obj, FREEZABLE_NO)
Expand Down
Loading