Skip to content

Commit

Permalink
actionpack#80: "generalizes ReadBytes and WriteBytes" (#81)
Browse files Browse the repository at this point in the history
* updates @synchronized nested function name

* WriteBytes -> Write

* ReadBytes -> Read

* updates ProcedureTest
  • Loading branch information
withtwoemms authored Oct 20, 2021
1 parent 6148eaf commit 5633bce
Show file tree
Hide file tree
Showing 16 changed files with 260 additions and 193 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ Keeping track of external system state is just impractical, but declaring intent
Intent can be declared using `Action` objects:

```python
>>> action = ReadBytes('path/to/some/file')
>>> action = Read('path/to/some/file')
```

An `Action` collection can be used to describe a procedure:

```python
>>> actions = [action,
... ReadBytes('path/to/some/other/file'),
... Read('path/to/some/other/file'),
... ReadInput('>>> how goes? <<<\n > '),
... MakeRequest('GET', 'http://google.com'),
... RetryPolicy(MakeRequest('GET', 'http://bad-connectivity.com'),
... max_retries=2,
... delay_between_attempts=2)
... WriteBytes('path/to/yet/another/file', b'sup')]
... Write('path/to/yet/another/file', 'sup')]
...
>>> procedure = Procedure(*actions)
```
Expand All @@ -51,7 +51,7 @@ The `Action` names are used as keys for convenient result lookup.
```python
>>> prompt = '>>> sure, I'll save it for ya.. <<<\n > '
>>> saveme = ReadInput(prompt).set(name='saveme')
>>> writeme = WriteBytes('path/to/yet/another/file', b'sup').set(name='writeme')
>>> writeme = Write('path/to/yet/another/file', 'sup').set(name='writeme')
>>> actions = [saveme, writeme]
>>> keyed_procedure = KeyedProcedure(*actions)
>>> results = keyed_procedure.execute()
Expand Down
8 changes: 4 additions & 4 deletions actionpack/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from actionpack.actions.call import Call
from actionpack.actions.pipeline import Pipeline
from actionpack.actions.make_request import MakeRequest
from actionpack.actions.read_bytes import ReadBytes
from actionpack.actions.read import Read
from actionpack.actions.read_input import ReadInput
from actionpack.actions.retry_policy import RetryPolicy
from actionpack.actions.serialization import Serialization
from actionpack.actions.write_bytes import WriteBytes
from actionpack.actions.write import Write


__all__ = [
'Call',
'MakeRequest',
'Pipeline',
'ReadBytes',
'Read',
'ReadInput',
'RetryPolicy',
'Serialization',
'WriteBytes'
'Write',
]
23 changes: 23 additions & 0 deletions actionpack/actions/read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations
from pathlib import Path

from actionpack import Action
from actionpack.action import Name


class Read(Action[Name, bytes]):
def __init__(self, filename: str, output_type: type = bytes):
if output_type not in [bytes, str]:
raise TypeError(f'Must be of type bytes or str: {output_type}')
self.output_type = output_type
self.path = Path(filename)

def instruction(self) -> bytes:
return self.path.read_bytes() if self.output_type is bytes else self.path.read_text()

def validate(self) -> Read[Name, bytes]:
if not self.path.exists():
raise FileNotFoundError(str(self.path))
if self.path.is_dir():
raise IsADirectoryError(str(self.path))
return self
20 changes: 0 additions & 20 deletions actionpack/actions/read_bytes.py

This file was deleted.

49 changes: 49 additions & 0 deletions actionpack/actions/write.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations
from pathlib import Path

from actionpack import Action
from actionpack.action import Name


class Write(Action[Name, int]):
def __init__(
self,
filename: str,
to_write: str,
prefix: str = None,
overwrite: bool = False,
append: bool = False,
):
prefix_type, to_write_type = type(prefix), type(to_write)
if prefix_type != to_write_type and to_write_type not in [bytes, str]:
raise TypeError(f'Must be of type {to_write_type}: {prefix_type}')

self.path = Path(filename)
self.to_write = to_write
self.prefix = prefix
self.append = append
self.overwrite = overwrite

def instruction(self) -> str:
if isinstance(self.to_write, bytes):
mode, msg = 'ab', self.prefix if self.prefix else b'' + self.to_write
elif isinstance(self.to_write, str):
mode, msg = 'a', f"{self.prefix if self.prefix else ''}{self.to_write}"
else:
raise TypeError(f'Must be of str or bytes: {self.to_write}')

if self.append:
rchar = b'\n' if mode == 'ab' else '\n'
self.path.open(mode).write(msg + rchar)
else:
self.path.write_bytes(msg) if isinstance(msg, bytes) else self.path.write_text(self.to_write)
return str(self.path.absolute())

def validate(self) -> Write[Name, int]:
if self.overwrite and self.append:
raise ValueError('Cannot overwrite and append simultaneously')
if self.path.exists() and not self.overwrite and not self.append:
raise FileExistsError(f'Cannot {str(self)} to {str(self.path)}')
if self.path.is_dir():
raise IsADirectoryError(str(self.path))
return self
29 changes: 0 additions & 29 deletions actionpack/actions/write_bytes.py

This file was deleted.

4 changes: 2 additions & 2 deletions actionpack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ def pickleable(obj) -> Optional[bytes]:
def synchronized(lock):
def wrap(f):
@wraps(f)
def newFunction(*args, **kw):
def lockedFunction(*args, **kw):
with lock:
return f(*args, **kw)
return newFunction
return lockedFunction
return wrap


Expand Down
11 changes: 7 additions & 4 deletions tests/actionpack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from io import BytesIO
from io import StringIO
from typing import Union

from actionpack import Action
from actionpack.action import Name
Expand All @@ -25,17 +26,19 @@ def validate(self):


class FakeFile:
def __init__(self, contents: bytes = bytes(), mode: str = None):
self.buffer = BytesIO(contents)
def __init__(self, contents=None, mode: str = None):
if not contents:
contents = bytes()
self.buffer = BytesIO(contents) if isinstance(contents, bytes) else StringIO(contents)
self.buffer.read()
self.mode = mode

def read(self):
self.buffer.seek(0)
return self.buffer.read()

def write(self, data: bytes):
if self.mode == 'wb':
def write(self, data):
if self.mode in ['wb', 'w']:
self.buffer.seek(0)
self.buffer.truncate()
self.buffer.write(data)
Expand Down
8 changes: 4 additions & 4 deletions tests/actionpack/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ def __init__(self, content: bytes = bytes(), status: int = 200):
self.status_code = status


class FakeWriteBytes(Action[Name, int]):
def __init__(self, file: FakeFile, bytes_to_write: bytes, delay: float):
self.bytes_to_write = bytes_to_write
class FakeWrite(Action[Name, int]):
def __init__(self, file: FakeFile, to_write: bytes, delay: float):
self.to_write = to_write
self.delay = delay
self.file = file

def instruction(self) -> int:
sleep(self.delay)
result = self.file.write(self.bytes_to_write)
result = self.file.write(self.to_write)
return result
12 changes: 6 additions & 6 deletions tests/actionpack/actions/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from actionpack.action import Result
from actionpack.actions import Pipeline
from actionpack.actions import ReadInput
from actionpack.actions import ReadBytes
from actionpack.actions import WriteBytes
from actionpack.actions import Read
from actionpack.actions import Write
from actionpack.actions.pipeline import Call
from actionpack.utils import Closure
from tests.actionpack import FakeFile
Expand All @@ -25,7 +25,7 @@ def test_Pipeline(self, mock_input, mock_exists, mock_file):
mock_exists.return_value = True
mock_input.return_value = filename

pipeline = Pipeline(ReadInput('Which file?'), ReadBytes)
pipeline = Pipeline(ReadInput('Which file?'), Read)
result = pipeline.perform()

self.assertIsInstance(result, Result)
Expand All @@ -47,11 +47,11 @@ def test_Pipeline_FittingType(self, mock_input, mock_exists, mock_file):
read_input = ReadInput('Which file?')
action_types = [
Pipeline.Fitting(
action=WriteBytes,
action=Write,
reaction=Call(Closure(bytes.decode)),
**{'append': True, 'bytes_to_write': question},
**{'append': True, 'to_write': question},
),
ReadBytes, # retrieve question from FakeFile
Read, # retrieve question from FakeFile
ReadInput # pose question to user
]
pipeline = Pipeline(read_input, *action_types, should_raise=True)
Expand Down
52 changes: 52 additions & 0 deletions tests/actionpack/actions/test_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pickle

from pathlib import Path
from unittest import TestCase
from unittest.mock import patch

from actionpack.action import Result
from actionpack.actions import Read
from actionpack.utils import pickleable


class ReadTest(TestCase):

contents = 'some file contents.'

@patch('pathlib.Path.read_bytes')
def test_can_Read_bytes(self, mock_input):
contents = self.contents.encode()
mock_input.return_value = contents
action = Read(__file__, output_type=bytes)
result = action.perform()
self.assertIsInstance(result, Result)
self.assertEqual(result.value, contents)

@patch('pathlib.Path.read_text')
def test_can_Read_string(self, mock_input):
mock_input.return_value = self.contents
action = Read(__file__, output_type=str)
result = action.perform()
self.assertIsInstance(result, Result)
self.assertEqual(result.value, self.contents)

@patch('pathlib.Path.read_bytes')
def test_can_ReadBytes_even_if_failure(self, mock_input):
contents = self.contents.encode()
mock_input.return_value = contents

invalid_file_result = Read('some/invalid/filepath').perform()
self.assertIsInstance(invalid_file_result, Result)
self.assertIsInstance(invalid_file_result.value, FileNotFoundError)

directory_result = Read(Path(__file__).parent).perform()
self.assertIsInstance(directory_result, Result)
self.assertIsInstance(directory_result.value, IsADirectoryError)

def test_can_pickle(self):
action = Read(__file__)
pickled = pickleable(action)
unpickled = pickle.loads(pickled)

self.assertTrue(pickleable(action))
self.assertEqual(unpickled.__dict__, action.__dict__)
43 changes: 0 additions & 43 deletions tests/actionpack/actions/test_read_bytes.py

This file was deleted.

Loading

0 comments on commit 5633bce

Please sign in to comment.