From 478532021e3213fda94fd41338053df9edc03ab2 Mon Sep 17 00:00:00 2001 From: Tom Ritchford Date: Mon, 11 May 2020 13:04:31 +0200 Subject: [PATCH] Unit test for help (fix #10) --- doc_safer.py | 4 +- safer.py | 11 -- test/get_help.py | 16 ++ test/help.txt | 361 +++++++++++++++++++++++++++++++++++++++++++++ test/test_doc.py | 18 ++- test/test_safer.py | 7 - 6 files changed, 395 insertions(+), 22 deletions(-) create mode 100644 test/get_help.py create mode 100644 test/help.txt diff --git a/doc_safer.py b/doc_safer.py index d90865f..44c5888 100644 --- a/doc_safer.py +++ b/doc_safer.py @@ -1,6 +1,6 @@ -from __future__ import print_function import inspect import safer +from test import get_help README_FILE = 'README.rst' @@ -28,6 +28,8 @@ def main(): with safer.printer(README_FILE) as print: print(make_doc()) + get_help.write_help() + BODY = """ {doc} diff --git a/safer.py b/safer.py index 61211eb..d22523c 100644 --- a/safer.py +++ b/safer.py @@ -496,17 +496,6 @@ def close(self): temporary file is deleted. """ -_DOC_FUNC = { - 'open': """ -A drop-in replacement for ``open()`` which returns a stream which only -overwrites the original file when close() is called, and only if there was no -failure""", - 'printer': """ -A context manager that yields a function that prints to the opened file, -only overwriting the original file at the exit of the context, -and only if there was no exception thrown""", -} - _DOC_WRITER_ARGS = """ ARGUMENTS stream: diff --git a/test/get_help.py b/test/get_help.py new file mode 100644 index 0000000..7bb4cb4 --- /dev/null +++ b/test/get_help.py @@ -0,0 +1,16 @@ +import os +import pydoc +import safer + +HELP_FILE = os.path.join(os.path.dirname(__file__), 'help.txt') + + +def get_help(): + items = [getattr(safer, k) for k in safer.__all__] + [safer] + return [pydoc.render_doc(i, title='Help on %s:') for i in items] + + +def write_help(): + with safer.open(HELP_FILE, 'w') as fp: + for h in get_help(): + fp.write(h) diff --git a/test/help.txt b/test/help.txt new file mode 100644 index 0000000..108e9ae --- /dev/null +++ b/test/help.txt @@ -0,0 +1,361 @@ +Help on function writer in module safer: + +wwrriitteerr(stream, is_binary=None, close_on_exit=False) + Write safely to file streams, sockets and callables. + + ``safer.writer`` yields an in-memory stream that you can write + to, but which is only written to the original stream if the + context finished without raising an exception. + + Because the actual writing happens when the context exits, it's possible + to block indefinitely if the underlying socket, stream or callable does. + + ARGUMENTS + stream: + A file stream, a socket, or a callable that will receive data + + is_binary: + Is ``stream`` a binary stream? + + If ``is_binary`` is ``None``, deduce whether it's a binary file from + the stream, or assume it's text otherwise. +Help on function open in module safer: + +ooppeenn(name, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, follow_symlinks=True, make_parents=False, delete_failures=True, temp_file=False) + A drop-in replacement for ``open()`` which returns a stream which only + overwrites the original file when close() is called, and only if there was + no failure + + If a stream ``fp`` return from ``safer.open()`` is used as a context + manager and an exception is raised, the property ``fp.safer_failed`` is + set to ``True``. + + In the method ``fp.close()``, if ``fp.safer_failed`` is *not* set, then the + temporary file is moved over the original file, successfully completing the + write. + + If ``fp.safer_failed`` is true, then if ``delete_failures`` is true, the + temporary file is deleted. + + + If the ``mode`` argument contains either ``'a'`` (append), or ``'+'`` + (update), then the original file will be copied to the temporary file + before writing starts. + + Note that ``safer`` uses an extra temporary file which is renamed over the + file only after the stream closes without failing. This uses as much disk + space as the old and new files put together. + + ARGUMENTS + make_parents: + If true, create the parent directory of the file if it doesn't exist + + delete_failures: + If true, the temporary file is deleted if there is an exception + + follow_symlinks: + If true, overwrite the file pointed to and not the symlink + + temp_file: + If true use a disk file and os.rename() at the end, otherwise + cache the writes in memory. If it's a string, use this as the + name of the temporary file, otherwise select one in the same + directory as the target file, or in the system tempfile for streams + that aren't files. + + The remaining arguments are the same as for built-in ``open()``. +Help on function closer in module safer: + +cclloosseerr(stream, is_binary=None, close_on_exit=False) + Like ``safer.writer()`` but with ``close_on_exit=True`` by default + + ARGUMENTS + stream: + A file stream, a socket, or a callable that will receive data + + is_binary: + Is ``stream`` a binary stream? + + If ``is_binary`` is ``None``, deduce whether it's a binary file from + the stream, or assume it's text otherwise. +Help on function printer in module safer: + +pprriinntteerr(name, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, follow_symlinks=True, make_parents=False, delete_failures=True, temp_file=False) + A context manager that yields a function that prints to the opened file, + only overwriting the original file at the exit of the context, + and only if there was no exception thrown + + + If the ``mode`` argument contains either ``'a'`` (append), or ``'+'`` + (update), then the original file will be copied to the temporary file + before writing starts. + + Note that ``safer`` uses an extra temporary file which is renamed over the + file only after the stream closes without failing. This uses as much disk + space as the old and new files put together. + + ARGUMENTS + make_parents: + If true, create the parent directory of the file if it doesn't exist + + delete_failures: + If true, the temporary file is deleted if there is an exception + + follow_symlinks: + If true, overwrite the file pointed to and not the symlink + + temp_file: + If true use a disk file and os.rename() at the end, otherwise + cache the writes in memory. If it's a string, use this as the + name of the temporary file, otherwise select one in the same + directory as the target file, or in the system tempfile for streams + that aren't files. + + The remaining arguments are the same as for built-in ``open()``. +Help on module safer: + +NNAAMMEE + safer + +DDEESSCCRRIIPPTTIIOONN + ✏️safer: a safer file opener ✏️ + ------------------------------- + + No more partial writes or corruption! For file streams, sockets or + any callable. + + Install ``safer`` from the command line with pip + (https://pypi.org/project/pip): ``pip install safer``. + + Tested on Python 3.4 and 3.8 + For Python 2.7, use https://github.com/rec/safer/tree/v2.0.5 + + See the Medium article `here. `_ + + ``safer`` does not force atomic writing of files! It is aimed at preventing + corrupt files, streams, socket connections or similar, but from to a programmer + error, not because of concurrent modification of files from other threads or + processes. See https://pypi.org/project/atomicwrites/ if you need atomic file + writing. + + * ``safer.writer()`` wraps an existing writer or socket and writes a whole + response or nothing, by caching written data in memory + + * ``safer.open()`` is a drop-in replacement for built-in ``open`` that + writes a whole file or nothing by caching written data on disk. + Unfortunately, disk caching does not work on Windows. + + * ``safer.closer()`` returns a stream like from ``safer.write()`` that also + closes the underlying stream or callable when it closes. + + * ``safer.printer()`` is ``safer.open()`` except that it yields a + a function that prints to the stream. Like ``safer.open()``, it + unfortunately does not work on Windows. + + ------------------ + + ``safer.open()`` + ================= + + ``safer.open()`` writes a whole file or nothing. It's a drop-in replacement for + built-in ``open()`` except that ``safer.open()`` leaves the original file + unchanged on failure. + + EXAMPLE + + .. code-block:: python + + # dangerous + with open(filename, 'w') as fp: + json.dump(data, fp) + # If an exception is raised, the file is empty or partly written + + # safer + with safer.open(filename, 'w') as fp: + json.dump(data, fp) + # If an exception is raised, the file is unchanged. + + + ``safer.open(filename)`` returns a file stream ``fp`` like ``open(filename)`` + would, except that ``fp`` writes to a temporary file in the same directory. + + If ``fp`` is used as a context manager and an exception is raised, then + ``fp.safer_failed`` is automatically set to ``True``. And when ``fp.close()`` + is called, the temporary file is moved over ``filename`` *unless* + ``fp.safer_failed`` is true. + + ------------------------------------ + + ``safer.writer()`` + ================== + + ``safer.writer()`` is like ``safer.open()`` except that it uses an existing + writer, a socket, or a callback. + + EXAMPLE + + .. code-block:: python + + sock = socket.socket(*args) + + # dangerous + try: + write_header(sock) + write_body(sock) + write_footer(sock) + except: + write_error(sock) # You already wrote the header! + + # safer + with safer.write(sock) as s: + write_header(s) + write_body(s) + write_footer(s) + except: + write_error(sock) # Nothing has been written + + ``safer.printer()`` + =================== + + ``safer.printer()`` is similar to ``safer.open()`` except it yields a function + that prints to the open file - it's very convenient for printing text. + + Like ``safer.open()``, if an exception is raised within its context manager, + the original file is left unchanged. + + EXAMPLE + + .. code-block:: python + + # dangerous + with open(file, 'w') as fp: + for item in items: + print(item, file=fp) + # Prints lines until the first exception + + # safer + with safer.printer(file) as print: + for item in items: + print(item) + # Either the whole file is written, or nothing + +FFUUNNCCTTIIOONNSS + cclloosseerr(stream, is_binary=None, close_on_exit=False) + Like ``safer.writer()`` but with ``close_on_exit=True`` by default + + ARGUMENTS + stream: + A file stream, a socket, or a callable that will receive data + + is_binary: + Is ``stream`` a binary stream? + + If ``is_binary`` is ``None``, deduce whether it's a binary file from + the stream, or assume it's text otherwise. + + ooppeenn(name, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, follow_symlinks=True, make_parents=False, delete_failures=True, temp_file=False) + A drop-in replacement for ``open()`` which returns a stream which only + overwrites the original file when close() is called, and only if there was + no failure + + If a stream ``fp`` return from ``safer.open()`` is used as a context + manager and an exception is raised, the property ``fp.safer_failed`` is + set to ``True``. + + In the method ``fp.close()``, if ``fp.safer_failed`` is *not* set, then the + temporary file is moved over the original file, successfully completing the + write. + + If ``fp.safer_failed`` is true, then if ``delete_failures`` is true, the + temporary file is deleted. + + + If the ``mode`` argument contains either ``'a'`` (append), or ``'+'`` + (update), then the original file will be copied to the temporary file + before writing starts. + + Note that ``safer`` uses an extra temporary file which is renamed over the + file only after the stream closes without failing. This uses as much disk + space as the old and new files put together. + + ARGUMENTS + make_parents: + If true, create the parent directory of the file if it doesn't exist + + delete_failures: + If true, the temporary file is deleted if there is an exception + + follow_symlinks: + If true, overwrite the file pointed to and not the symlink + + temp_file: + If true use a disk file and os.rename() at the end, otherwise + cache the writes in memory. If it's a string, use this as the + name of the temporary file, otherwise select one in the same + directory as the target file, or in the system tempfile for streams + that aren't files. + + The remaining arguments are the same as for built-in ``open()``. + + pprriinntteerr(name, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, follow_symlinks=True, make_parents=False, delete_failures=True, temp_file=False) + A context manager that yields a function that prints to the opened file, + only overwriting the original file at the exit of the context, + and only if there was no exception thrown + + + If the ``mode`` argument contains either ``'a'`` (append), or ``'+'`` + (update), then the original file will be copied to the temporary file + before writing starts. + + Note that ``safer`` uses an extra temporary file which is renamed over the + file only after the stream closes without failing. This uses as much disk + space as the old and new files put together. + + ARGUMENTS + make_parents: + If true, create the parent directory of the file if it doesn't exist + + delete_failures: + If true, the temporary file is deleted if there is an exception + + follow_symlinks: + If true, overwrite the file pointed to and not the symlink + + temp_file: + If true use a disk file and os.rename() at the end, otherwise + cache the writes in memory. If it's a string, use this as the + name of the temporary file, otherwise select one in the same + directory as the target file, or in the system tempfile for streams + that aren't files. + + The remaining arguments are the same as for built-in ``open()``. + + wwrriitteerr(stream, is_binary=None, close_on_exit=False) + Write safely to file streams, sockets and callables. + + ``safer.writer`` yields an in-memory stream that you can write + to, but which is only written to the original stream if the + context finished without raising an exception. + + Because the actual writing happens when the context exits, it's possible + to block indefinitely if the underlying socket, stream or callable does. + + ARGUMENTS + stream: + A file stream, a socket, or a callable that will receive data + + is_binary: + Is ``stream`` a binary stream? + + If ``is_binary`` is ``None``, deduce whether it's a binary file from + the stream, or assume it's text otherwise. + +DDAATTAA + ____aallll____ = ('writer', 'open', 'closer', 'printer') + +VVEERRSSIIOONN + 3.1.2 + +FFIILLEE + /code/safer/safer.py + diff --git a/test/test_doc.py b/test/test_doc.py index 4582981..3fdfd97 100644 --- a/test/test_doc.py +++ b/test/test_doc.py @@ -4,6 +4,7 @@ import io import platform from readme_renderer import rst +from test import get_help README_TEXT = Path(doc_safer.README_FILE).read_text() @@ -17,6 +18,17 @@ def test_make_doc(self): def test_rendering(self): out = io.StringIO() actual = rst.render(README_TEXT, out) - print('XXX') - print(out.getvalue()) - assert actual is not None + if actual is None: + print('Rendering error!') + print(out.getvalue()) + assert False + + def test_help(self): + with open(get_help.HELP_FILE) as fp: + actual = fp.read() + + expected = ''.join(get_help.get_help()) + + actual = actual.splitlines()[:-2] + expected = expected.splitlines()[:-2] + assert expected == actual diff --git a/test/test_safer.py b/test/test_safer.py index 9cc742e..3990974 100644 --- a/test/test_safer.py +++ b/test/test_safer.py @@ -3,7 +3,6 @@ from unittest import TestCase import functools import os -import pydoc import safer import stat @@ -184,12 +183,6 @@ def test_int_filename(self): arg = m.exception.args[0] assert arg == '`name` must be string, not int' - def test_help(self): - for name in safer.__all__: - func = getattr(safer, name) - value = pydoc.render_doc(func, title='%s') - assert value.startswith('function %s in module safer' % name) - @temps_test def test_line_buffering(self, safer_open): temp_file = safer_open is not safer.open