diff --git a/docs/index.rst b/docs/index.rst index 34808a7..d2a3904 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -366,6 +366,10 @@ Release Notes properly its return type breaking code completion in some LSP servers for the returned object. +* Fix ``Box.put()`` and ``picobox.put()`` to require either ``value`` + or ``factory`` argument. Previously, they could have been invoked with ``key`` + argument only, which makes no sense and causes runtime issues later on. + 3.0.0 ````` diff --git a/src/picobox/_box.py b/src/picobox/_box.py index cc56892..3c5aaca 100644 --- a/src/picobox/_box.py +++ b/src/picobox/_box.py @@ -79,8 +79,14 @@ def put( a class that implements :class:`Scope` interface. :raises ValueError: If both `value` and `factory` are passed. """ - if value is not _unset and (factory is not _unset or scope is not _unset): - raise ValueError("either 'value' or 'factory'/'scope' pair must be passed") + if value is _unset and factory is _unset: + raise TypeError("Box.put() missing 1 required argument: either 'value' or 'factory'") + + if value is not _unset and factory is not _unset: + raise TypeError("Box.put() takes either 'value' or 'factory', not both") + + if value is not _unset and scope is not _unset: + raise TypeError("Box.put() takes 'scope' when 'factory' provided") # Value is a syntax sugar Box supports to store objects "As Is" # with singleton scope. In other words it's essentially the same diff --git a/tests/test_box.py b/tests/test_box.py index 3609373..9b7cfb4 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -96,28 +96,43 @@ def fn(a): assert testbox.get("b") == 14 +def test_box_put_value_factory_required(boxclass): + testbox = boxclass() + + with pytest.raises(TypeError) as excinfo: + testbox.put("the-key") + + assert str(excinfo.value) == ( + "Box.put() missing 1 required argument: either 'value' or 'factory'" + ) + + def test_box_put_value_and_factory(boxclass): testbox = boxclass() - with pytest.raises(ValueError) as excinfo: + with pytest.raises(TypeError) as excinfo: testbox.put("the-key", 42, factory=object) - excinfo.match("either 'value' or 'factory'/'scope' pair must be passed") + + assert str(excinfo.value) == "Box.put() takes either 'value' or 'factory', not both" def test_box_put_value_and_scope(boxclass): testbox = boxclass() - with pytest.raises(ValueError) as excinfo: + with pytest.raises(TypeError) as excinfo: testbox.put("the-key", 42, scope=picobox.threadlocal) - excinfo.match("either 'value' or 'factory'/'scope' pair must be passed") + + assert str(excinfo.value) == "Box.put() takes 'scope' when 'factory' provided" def test_box_get_keyerror(boxclass): testbox = boxclass() - with pytest.raises(KeyError, match="the-key"): + with pytest.raises(KeyError) as excinfo: testbox.get("the-key") + assert str(excinfo.value) == "'the-key'" + def test_box_get_default(boxclass): testbox = boxclass() @@ -421,7 +436,7 @@ def fn(a, b): with pytest.raises(KeyError) as excinfo: fn(1) - excinfo.match("b") + assert str(excinfo.value) == "'b'" def test_box_pass_optimization(boxclass, request): diff --git a/tests/test_stack.py b/tests/test_stack.py index 30965b9..a6be870 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -70,22 +70,36 @@ def fn(a): assert teststack.get("b") == 14 +def test_box_put_value_factory_required(boxclass, teststack): + testbox = boxclass() + + with teststack.push(testbox): + with pytest.raises(TypeError) as excinfo: + teststack.put("the-key") + + assert str(excinfo.value) == ( + "Box.put() missing 1 required argument: either 'value' or 'factory'" + ) + + def test_box_put_value_and_factory(boxclass, teststack): testbox = boxclass() with teststack.push(testbox): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(TypeError) as excinfo: teststack.put("the-key", 42, factory=object) - excinfo.match("either 'value' or 'factory'/'scope' pair must be passed") + + assert str(excinfo.value) == "Box.put() takes either 'value' or 'factory', not both" def test_box_put_value_and_scope(boxclass, teststack): testbox = boxclass() with teststack.push(testbox): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(TypeError) as excinfo: teststack.put("the-key", 42, scope=picobox.threadlocal) - excinfo.match("either 'value' or 'factory'/'scope' pair must be passed") + + assert str(excinfo.value) == "Box.put() takes 'scope' when 'factory' provided" def test_box_put_runtimeerror(boxclass, teststack):