From 2a67e139cf600216046ad507923f548f9ecca274 Mon Sep 17 00:00:00 2001 From: "Emmanuel I. Obi" Date: Fri, 21 Jan 2022 00:17:21 -0600 Subject: [PATCH] actionpack#89: "handles failed action instantiation" (#102) * 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 --- README.md | 5 +++-- actionpack/action.py | 27 ++++++++++++++++++----- actionpack/procedure.py | 2 +- noxfile.py | 1 - tests/actionpack/__init__.py | 4 ++++ tests/actionpack/actions/test_pipeline.py | 13 +++++++++++ tests/actionpack/actions/test_write.py | 24 ++++++++++++++------ tests/actionpack/test_action.py | 7 ++++++ 8 files changed, 66 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 54ffda4..a2340fb 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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') diff --git a/actionpack/action.py b/actionpack/action.py index f5d01d0..3a79c7c 100644 --- a/actionpack/action.py +++ b/actionpack/action.py @@ -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 @@ -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'' + class DependencyCheck: def __init__(self, cls, requirement: str = None): if not requirement: diff --git a/actionpack/procedure.py b/actionpack/procedure.py index 0871429..0d9eb73 100644 --- a/actionpack/procedure.py +++ b/actionpack/procedure.py @@ -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) diff --git a/noxfile.py b/noxfile.py index 562ffed..c2669fb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -20,7 +20,6 @@ external = False if USEVENV else True supported_python_versions = [ - '3.6', '3.7', '3.8', '3.9', diff --git a/tests/actionpack/__init__.py b/tests/actionpack/__init__.py index 1885078..75af726 100644 --- a/tests/actionpack/__init__.py +++ b/tests/actionpack/__init__.py @@ -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'} diff --git a/tests/actionpack/actions/test_pipeline.py b/tests/actionpack/actions/test_pipeline.py index 271f1dc..506f55e 100644 --- a/tests/actionpack/actions/test_pipeline.py +++ b/tests/actionpack/actions/test_pipeline.py @@ -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') diff --git a/tests/actionpack/actions/test_write.py b/tests/actionpack/actions/test_write.py index a64b08a..6414968 100644 --- a/tests/actionpack/actions/test_write.py +++ b/tests/actionpack/actions/test_write.py @@ -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 @@ -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): diff --git a/tests/actionpack/test_action.py b/tests/actionpack/test_action.py index 1027260..40d19e4 100644 --- a/tests/actionpack/test_action.py +++ b/tests/actionpack/test_action.py @@ -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)