Skip to content

Commit

Permalink
actionpack#89: "handles failed action instantiation" (#102)
Browse files Browse the repository at this point in the history
* adds Pipeline test

* Procedure type check error displays type name

* clarifies Pipeline test

* adds Action.Guise test

* reforms ActionType and introduces Action.Guise

* updates Write test to leverage Action.Guise

* updates README in-line with actionpack#95

* adds pypi README badge

* removes unsupported python version from noxfile
  • Loading branch information
withtwoemms authored Jan 21, 2022
1 parent 2355916 commit 2a67e13
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 17 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![tests](https://github.com/withtwoemms/actionpack/workflows/tests/badge.svg)](https://github.com/withtwoemms/actionpack/actions?query=workflow%3Atests)
[![codecov](https://codecov.io/gh/withtwoemms/actionpack/branch/main/graph/badge.svg?token=27Z4W0COFH)](https://codecov.io/gh/withtwoemms/actionpack)
[![publish](https://github.com/withtwoemms/actionpack/workflows/publish/badge.svg)](https://github.com/withtwoemms/actionpack/actions?query=workflow%3Apublish)
[![PyPI version](https://badge.fury.io/py/actionpack.svg)](https://badge.fury.io/py/actionpack)

# Overview

Expand Down Expand Up @@ -71,7 +72,7 @@ An `Action` collection can be used to describe a procedure:
... delay_between_attempts=2)
... Write('path/to/yet/another/file', 'sup')]
...
>>> procedure = Procedure(*actions)
>>> procedure = Procedure(actions)
```

And a `Procedure` can be executed synchronously or otherwise:
Expand All @@ -91,7 +92,7 @@ The `Action` names are used as keys for convenient result lookup.
>>> saveme = ReadInput(prompt).set(name='saveme')
>>> writeme = Write('path/to/yet/another/file', 'sup').set(name='writeme')
>>> actions = [saveme, writeme]
>>> keyed_procedure = KeyedProcedure(*actions)
>>> keyed_procedure = KeyedProcedure(actions)
>>> results = keyed_procedure.execute()
>>> keyed_results = dict(results)
>>> first, second = keyed_results.get('saveme'), keyed_results.get('writeme')
Expand Down
27 changes: 21 additions & 6 deletions actionpack/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,22 @@ class OutcomeMustBeOfTypeEither(Exception):

class ActionType(type):

def _instruction(self):
raise self.failure

def __call__(self, *args, **kwargs):
instance = super().__call__(*args, **kwargs)
failure = None

def instruction():
raise failure

try:
instance = super().__call__(*args, **kwargs)
except Exception as e:
return Action.Guise(e)

params = args + tuple(kwargs.values())
for param in params:
if issubclass(type(param), Exception):
self.failure = param
setattr(instance, 'instruction', self._instruction)
failure = param
setattr(instance, 'instruction', instruction)

return instance

Expand Down Expand Up @@ -145,6 +150,16 @@ def __repr__(self):
class NotComparable(Exception):
pass

class Guise:
def __init__(self, failure: Exception):
self.failure = failure

def perform(self, should_raise: bool = False) -> Result:
return Result(Left(self.failure))

def __repr__(self):
return f'<Action.Guise[{self.failure.__class__.__name__}]>'

class DependencyCheck:
def __init__(self, cls, requirement: str = None):
if not requirement:
Expand Down
2 changes: 1 addition & 1 deletion actionpack/procedure.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Procedure(Generic[Name, Outcome]):

def __init__(self, actions: Iterable[Action[Name, Outcome]]):
if not (isinstance(actions, Iterator) or isinstance(actions, Iterable)):
raise TypeError(f'Actions must be iterable. Received {type(actions)}.')
raise TypeError(f'Actions must be iterable. Received {type(actions).__name__}.')

self.actions, self._actions, self.__actions = tee(actions, 3)

Expand Down
1 change: 0 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

external = False if USEVENV else True
supported_python_versions = [
'3.6',
'3.7',
'3.8',
'3.9',
Expand Down
4 changes: 4 additions & 0 deletions tests/actionpack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ class FakeAction(Action[Name, Outcome]):
def __init__(
self,
name: Name = None,
typecheck = None,
instruction_provider: Callable = None,
):
if typecheck:
raise TypeError(str(typecheck))

self.name = name
self.instruction_provider = instruction_provider
self.state = {'this': 'state'}
Expand Down
13 changes: 13 additions & 0 deletions tests/actionpack/actions/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ def test_Pipeline(self, mock_input, mock_exists, mock_file):
self.assertIsInstance(result, Result)
self.assertEqual(result.value, contents)

@patch('pathlib.Path.exists')
def test_first_Pipeline_failure_prevents_further_execution(self, mock_exists):
bad_filename = 'bad/filename.txt'

mock_exists.return_value = False

pipeline = Pipeline(Read(bad_filename), ReadInput)
result = pipeline.perform()

self.assertIsInstance(result, Result)
self.assertIsInstance(result.value, FileNotFoundError)
self.assertEqual(str(result.value), bad_filename)

@patch('pathlib.Path.open')
@patch('pathlib.Path.exists')
@patch('builtins.input')
Expand Down
24 changes: 17 additions & 7 deletions tests/actionpack/actions/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from unittest import TestCase
from unittest.mock import patch

from actionpack import Action
from actionpack.action import Result
from actionpack.actions import Write
from actionpack.utils import pickleable
Expand Down Expand Up @@ -36,17 +37,26 @@ def test_can_Write_string(self, mock_output):
self.assertIsInstance(result, Result)
self.assertEqual(result.value, self.absfilepath)

def test_can_Write_raises_when_given_an_Exception_to_write(self):
def test_Write_raises_when_given_an_Exception_to_write(self):
exception = Exception('some error.')
with self.assertRaises(type(exception)):
Write(self.absfilepath, exception).perform(should_raise=True)

def test_can_Write_raises_when_given_an_prefix_of_different_type(self):
Write(self.absfilepath, to_write='data', prefix='prefix') # should not fail
with self.assertRaises(TypeError):
Write(self.absfilepath, to_write=123, prefix='prefix')
with self.assertRaises(TypeError):
Write(self.absfilepath, to_write=123, prefix=123)
def test_Write_does_not_raise_when_instantiated_with_unexpected_types(self):
write1 = Write(self.absfilepath, to_write='data', prefix='prefix')
self.assertIsInstance(write1, Write)

write2 = Write(self.absfilepath, to_write=123, prefix='prefix')
self.assertIsInstance(write2, Action.Guise)
result2 = write2.perform()
self.assertIsInstance(result2, Result)
self.assertIsInstance(result2.value, TypeError)

write3 = Write(self.absfilepath, to_write='123', prefix=123)
self.assertIsInstance(write3, Action.Guise)
result3 = write3.perform()
self.assertIsInstance(result3, Result)
self.assertIsInstance(result3.value, TypeError)

@patch('pathlib.Path.open')
def test_can_overWrite_bytes(self, mock_output):
Expand Down
7 changes: 7 additions & 0 deletions tests/actionpack/test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def test_Result_success_is_immutable(self):
with self.assertRaises(AttributeError):
failure.successful = 'maybe?'

def test_Action_Guise(self):
guise = FakeAction(typecheck='Action instantiation fails.')
result = guise.perform()
self.assertIsInstance(guise, Action.Guise)
self.assertIsInstance(result, Result)
self.assertIsInstance(result.value, Exception)

def test_Action_can_be_serialized(self):
action = FakeAction()
pickled = pickle.dumps(action)
Expand Down

0 comments on commit 2a67e13

Please sign in to comment.