From d0e39c52c131721efcecf1eefc09192941f39f71 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Wed, 30 Sep 2015 12:01:53 -0500 Subject: [PATCH 01/54] bump version number post-release --- docs/source/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2ca3f5a..4ebf1ce 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,6 @@ master_doc = 'index' project = u'Effect' copyright = u'2015, Christopher Armstrong' -version = release = '0.10.1' +version = release = '0.10.1+' html_theme = 'sphinx_rtd_theme' diff --git a/setup.py b/setup.py index 1b7d602..bae56da 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name="effect", - version="0.10.1", + version="0.10.1+", description="pure effects for Python", long_description=open('README.rst').read(), url="http://github.com/python-effect/effect/", From fbfb6d660ca3a9a7e79ec2b2f366fea2d2dd1a72 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Wed, 30 Sep 2015 15:12:40 -0500 Subject: [PATCH 02/54] convert test_do.py to use pytest API --- effect/test_do.py | 247 +++++++++++++++++++++++----------------------- 1 file changed, 125 insertions(+), 122 deletions(-) diff --git a/effect/test_do.py b/effect/test_do.py index 789ace6..8d45fd4 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -6,9 +6,6 @@ import six -from testtools import TestCase -from testtools.matchers import raises as match_raises, MatchesException - from . import ( ComposedDispatcher, Constant, Effect, Error, TypeDispatcher, base_dispatcher, sync_perform, sync_performer) @@ -19,125 +16,131 @@ def perf(e): return sync_perform(base_dispatcher, e) -class DoTests(TestCase): - - def test_do_non_gf(self): - """When do is passed a non-generator function, it raises an error.""" - f = lambda: None - self.assertThat( - lambda: perf(do(f)()), - match_raises(TypeError( - "%r is not a generator function. It returned None." % (f,) - ))) - - def test_do_return(self): - """ - When a @do function yields a do_return, the given value becomes the - eventual result. - """ - @do - def f(): - yield do_return("hello") - self.assertEqual(perf(f()), "hello") - - def test_yield_effect(self): - """Yielding an effect in @do results in the Effect's result.""" - @do - def f(): - x = yield Effect(Constant(3)) - yield do_return(x) - self.assertEqual(perf(f()), 3) - - def test_fall_off_the_end(self): - """Falling off the end results in None.""" - @do - def f(): - yield Effect(Constant(3)) - self.assertEqual(perf(f()), None) - - def test_yield_non_effect(self): - """Yielding a non-Effect results in a TypeError.""" - @do - def f(): - yield 1 - result = f() - e = self.assertRaises(TypeError, lambda: perf(result)) - self.assertTrue( - str(e).startswith( - '@do functions must only yield Effects or results of ' - 'do_return. Got 1 from Date: Wed, 30 Sep 2015 15:14:07 -0500 Subject: [PATCH 03/54] add a test that ensures we can return effects from @do functions --- effect/test_do.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/effect/test_do.py b/effect/test_do.py index 8d45fd4..a7ad88f 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -36,6 +36,13 @@ def f(): assert perf(f()) == "hello" +def test_do_return_effect(): + @do + def f(): + yield do_return(Effect(Constant("hello"))) + assert perf(f()) == "hello" + + def test_yield_effect(): """Yielding an effect in @do results in the Effect's result.""" @do From 826cbc5b6917683ba6ddf70cd693d3028ecdf226 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Mon, 2 Nov 2015 09:22:31 -0600 Subject: [PATCH 04/54] add a reminder to add GitHub release notes when I do a release --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f532fff..c8844e0 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,10 @@ build-dist: upload-dist: twine upload dist/* echo - echo "Don't forget to add a git tag." - echo "And don't forget to bump the version in setup.py and docs/source/conf.py." + echo "Don't forget to:" + echo "- add a git tag." + echo "- add release notes to GitHub" + echo "- bump the version in setup.py and docs/source/conf.py." doc: rm -rf docs/build From a8cd04eeabd4d0a0f1e68bce24042136dddec9b0 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 23 Dec 2016 10:20:06 -0800 Subject: [PATCH 05/54] add some testing goodies --- effect/test_testing.py | 30 +++++++++++++++++++ effect/testing.py | 65 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/effect/test_testing.py b/effect/test_testing.py index 3871f35..cb9933a 100644 --- a/effect/test_testing.py +++ b/effect/test_testing.py @@ -27,7 +27,10 @@ EQDispatcher, EQFDispatcher, SequenceDispatcher, + const, + conste, fail_effect, + intent_func, parallel_sequence, perform_sequence, resolve_effect, @@ -462,3 +465,30 @@ def test_parallel_sequence_must_be_parallel(): with pytest.raises(FoldError) as excinfo: perform_sequence(seq, p) assert excinfo.value.wrapped_exception[0] is AssertionError + + +def test_const(): + """ + :func:`const` takes an argument but returns fixed value + """ + assert const(2)(MyIntent("whatever")) == 2 + assert const("text")(OtherIntent("else")) == "text" + + +def test_conste(): + """ + :func:`conste` takes an argument but always raises given exception + """ + func = conste(ValueError("boo")) + with pytest.raises(ValueError): + func(MyIntent("yo")) + + +def test_intent_func(): + """ + :func:`intent_func` returns function that returns Effect of tuple of passed arg + and its args. + """ + func = intent_func("myfunc") + assert func(2, 3) == Effect(("myfunc", 2, 3)) + assert func("text", 3, None) == Effect(("myfunc", "text", 3, None)) diff --git a/effect/testing.py b/effect/testing.py index 62c7674..035d2a9 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -12,7 +12,7 @@ import attr -from ._base import Effect, guard, _Box, NoPerformerFoundError +from ._base import Effect, guard, _Box, NoPerformerFoundError, raise_ from ._sync import NotSynchronousError, sync_perform, sync_performer from ._intents import Constant, Error, Func, ParallelEffects, base_dispatcher @@ -30,6 +30,9 @@ 'ESConstant', 'ESError', 'ESFunc', 'resolve_stubs', 'resolve_stub', + 'const', + 'conste', + 'intent_func', ] @@ -465,7 +468,6 @@ def consume(self): def noop(intent): """ - Return None. This is just a handy way to make your intent sequences (as used by :func:`perform_sequence`) more concise when the effects you're expecting in a test don't return a result (and are instead only performed @@ -478,3 +480,62 @@ def noop(intent): """ return None + + +def const(value): + """ + Return function that takes an argument but always return given `value`. + Useful when creating sequence used by :func:`perform_sequence`. For example, + + >>> dt = datetime(1970, 1, 1) + >>> seq = [(Func(datetime.now), const(dt))] + + :param value: This will be returned when called by returned function + :return: ``callable`` that takes an arg and always returns ``value`` + """ + return lambda intent: value + + +def conste(excp): + """ + Like :func:`const` but takes and exception and returns function that raises + the exception + + :param excp: Exception that will be raised + :type: :obj:`Exception` + :return: ``callable`` that will raise given exception + """ + return lambda intent: raise_(excp) + + +def intent_func(fname): + """ + Return function that returns Effect of tuple of fname and its args. Useful + in writing tests that expect intent based on args. For example, if you are + testing following function:: + + @do + def code_under_test(arg1, arg2, eff_returning_func=eff_returning_func): + r = yield Effect(MyIntent('a')) + r2 = yield eff_returning_func(arg1, arg2) + yield do_return((r, r2)) + + you will need to know the intents which ``eff_returning_func`` generates + to test this using :func:`perform_sequence`. You can avoid that by doing:: + + def test_code(): + test_eff_func = intent_func("erf") + seq = [ + (MyIntent('a'), const('result1')), + (("erf", 'a1', 'a2'), const('result2')) + ] + eff = code_under_test('a1', 'a2', eff_returning_func=test_eff_func) + assert perform_sequence(seq, eff) == ('result1', 'result2') + + Here, the ``seq`` ensures that ``eff_returning_func`` is called with arguments + ``a1`` and ``a2``. + + :param str fname: First member of intent tuple returned + :return: ``callable`` with multiple positional arguments + """ + return lambda *a: Effect((fname,) + a) From 2b9241b31a388f52736d55aa455bb55c0ad718d9 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 23 Dec 2016 10:20:45 -0800 Subject: [PATCH 06/54] lint --- effect/testing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/effect/testing.py b/effect/testing.py index 035d2a9..d2b49e7 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -164,6 +164,7 @@ def test_code(): sequence dispatcher. """ perf = partial(perform_sequence, fallback_dispatcher=fallback_dispatcher) + def performer(intent): if len(intent.effects) != len(parallel_seqs): raise AssertionError( From 23ea45fa76ae2167382faef1f2307c6a7eebf2d2 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 23 Dec 2016 10:23:23 -0800 Subject: [PATCH 07/54] install readthedocs sphinx theme --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c5958f5..1419dea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: install: - pip install . - pip install -r dev-requirements.txt - - pip install sphinx + - pip install sphinx sphinx_rtd_theme script: - make lint - py.test From de540ff8cf82369e29245ea225a7c799a46b9775 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Tue, 27 Dec 2016 10:28:23 -0800 Subject: [PATCH 08/54] drop 2.6 support --- .travis.yml | 3 +-- README.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c5958f5..fe21b23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,12 @@ sudo: false language: python python: - "2.7" - - "2.6" - "3.4" - "pypy" install: - pip install . - pip install -r dev-requirements.txt - - pip install sphinx + - pip install sphinx sphinx_rtd_theme script: - make lint - py.test diff --git a/README.rst b/README.rst index 33bdd40..644ef94 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ the effects (that is, IO or state manipulation) in your code. Documentation is available at https://effect.readthedocs.org/, and its PyPI page is https://pypi.python.org/pypi/effect. -It `supports`_ both Python 2.6 and up, and 3.4 and up, as well as PyPy. +It `supports`_ both Python 2.7 and up, and 3.4 and up, as well as PyPy. .. _`supports`: https://travis-ci.org/python-effect/effect From 9e7b97c65ccabaf963f27b2057f62e3841eb0e7c Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 23 Dec 2016 10:20:45 -0800 Subject: [PATCH 09/54] lint --- effect/testing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/effect/testing.py b/effect/testing.py index 62c7674..75e815a 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -161,6 +161,7 @@ def test_code(): sequence dispatcher. """ perf = partial(perform_sequence, fallback_dispatcher=fallback_dispatcher) + def performer(intent): if len(intent.effects) != len(parallel_seqs): raise AssertionError( From 3e17932ee7306c32932031eb6a671c81c831e2de Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Tue, 27 Dec 2016 10:37:25 -0800 Subject: [PATCH 10/54] add 3.5 support too --- .travis.yml | 1 + README.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fe21b23..4eacaa4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ language: python python: - "2.7" - "3.4" + - "3.5" - "pypy" install: - pip install . diff --git a/README.rst b/README.rst index 644ef94..49a36a3 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ the effects (that is, IO or state manipulation) in your code. Documentation is available at https://effect.readthedocs.org/, and its PyPI page is https://pypi.python.org/pypi/effect. -It `supports`_ both Python 2.7 and up, and 3.4 and up, as well as PyPy. +It `supports`_ both Python 2.7 and up, and 3.5 and up, as well as PyPy. .. _`supports`: https://travis-ci.org/python-effect/effect From 660bda7ca88b3de97c96acea88f43f646a7c256f Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Tue, 27 Dec 2016 11:03:27 -0800 Subject: [PATCH 11/54] mention versions supported explicitly --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 49a36a3..b78eac9 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ the effects (that is, IO or state manipulation) in your code. Documentation is available at https://effect.readthedocs.org/, and its PyPI page is https://pypi.python.org/pypi/effect. -It `supports`_ both Python 2.7 and up, and 3.5 and up, as well as PyPy. +It `supports`_ Python 2.7, 3.4 and 3.5 as well as PyPy. .. _`supports`: https://travis-ci.org/python-effect/effect From 8b9fbbdc7ea805dd271e1b410e6abff57c8edd0b Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Tue, 27 Dec 2016 17:27:27 -0800 Subject: [PATCH 12/54] add nested_sequence function and some doc changes --- effect/test_testing.py | 33 ++++++++++++++++++++++++++++ effect/testing.py | 49 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/effect/test_testing.py b/effect/test_testing.py index cb9933a..bb4e24e 100644 --- a/effect/test_testing.py +++ b/effect/test_testing.py @@ -21,6 +21,7 @@ from .do import do, do_return from .fold import FoldError, sequence from .testing import ( + _ANY, ESConstant, ESError, ESFunc, @@ -31,6 +32,7 @@ conste, fail_effect, intent_func, + nested_sequence, parallel_sequence, perform_sequence, resolve_effect, @@ -467,6 +469,37 @@ def test_parallel_sequence_must_be_parallel(): assert excinfo.value.wrapped_exception[0] is AssertionError +def test_nested_sequence(): + """ + :func:`nested_sequence` returns sequence performer function for an intent + that wraps an effect. + """ + + @attr.s + class WrappedIntent(object): + effect = attr.ib() + value = attr.ib() + + @do + def internal(): + yield Effect(1) + yield Effect(2) + yield do_return("wrap") + + @do + def code_under_test(): + r = yield Effect(WrappedIntent(internal(), "field")) + r2 = yield Effect(MyIntent("a")) + yield do_return((r, r2)) + + seq = [ + (WrappedIntent(_ANY, "field"), nested_sequence([(1, const("r1")), (2, const("r2"))])), + (MyIntent("a"), const("result2")) + ] + eff = code_under_test() + assert perform_sequence(seq, eff) == ('wrap', 'result2') + + def test_const(): """ :func:`const` takes an argument but returns fixed value diff --git a/effect/testing.py b/effect/testing.py index d2b49e7..65dba72 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -8,6 +8,7 @@ from contextlib import contextmanager from functools import partial +from operator import attrgetter import sys import attr @@ -20,8 +21,13 @@ __all__ = [ 'perform_sequence', + 'parallel_sequence', + 'nested_sequence', 'SequenceDispatcher', 'noop', + 'const', + 'conste', + 'intent_func', 'resolve_effect', 'fail_effect', 'EQDispatcher', @@ -30,9 +36,6 @@ 'ESConstant', 'ESError', 'ESFunc', 'resolve_stubs', 'resolve_stub', - 'const', - 'conste', - 'intent_func', ] @@ -86,6 +89,7 @@ def test_code(): :param fallback_dispatcher: A dispatcher to use for intents that aren't found in the sequence. if None is provided, ``base_dispatcher`` is used. + :return: Result of performed sequence """ def fmt_log(): next_item = '' @@ -162,6 +166,8 @@ def test_code(): what :func:`perform_sequence` accepts. :param fallback_dispatcher: an optional dispatcher to compose onto the sequence dispatcher. + :return: (intent, performer) tuple as expected by :func:`perform_sequence` + where intent is ParallelEffects object """ perf = partial(perform_sequence, fallback_dispatcher=fallback_dispatcher) @@ -467,6 +473,43 @@ def consume(self): [x[0] for x in self.sequence])) +def nested_sequence(seq, get_effect=attrgetter('effect'), + fallback_dispatcher=base_dispatcher): + """ + Return a function of Intent -> a that performs an effect retrieved from the + intent (by accessing its `effect` attribute, by default) with the given + intent-sequence. + + A demonstration is best:: + + SequenceDispatcher([ + (BoundFields(effect=mock.ANY, fields={...}), + nested_sequence([(SomeIntent(), perform_some_intent)])) + ]) + + The point is that sometimes you have an intent that wraps another effect, + and you want to ensure that the nested effects follow some sequence in the + context of that wrapper intent. + + ``get_effect`` defaults to ``attrgetter('effect')``, so you can override it if + your intent stores its nested effect in a different attribute. Or, more + interestingly, if it's something other than a single effect, e.g. for + ParallelEffects see the :func:`parallel_sequence` function. + + :param list seq: sequence of intents like :obj:`SequenceDispatcher` takes + :param get_effect: callable to get the inner effect from the wrapper + intent. + :param fallback_dispatcher: an optional dispatcher to compose onto the + sequence dispatcher. + :return: ``callable`` that can be used as performer of a wrapped intent + """ + def performer(intent): + effect = get_effect(intent) + return perform_sequence(seq, effect, fallback_dispatcher=fallback_dispatcher) + + return performer + + def noop(intent): """ Return None. This is just a handy way to make your intent sequences (as From 2c525b3ff060b289694e45b0617964fa9bbba8c0 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Mon, 2 Jan 2017 12:50:31 -0600 Subject: [PATCH 13/54] bump to 0.11.0 --- docs/source/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4ebf1ce..7a86446 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,6 @@ master_doc = 'index' project = u'Effect' copyright = u'2015, Christopher Armstrong' -version = release = '0.10.1+' +version = release = '0.11.0' html_theme = 'sphinx_rtd_theme' diff --git a/setup.py b/setup.py index bae56da..e6812ad 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name="effect", - version="0.10.1+", + version="0.11.0", description="pure effects for Python", long_description=open('README.rst').read(), url="http://github.com/python-effect/effect/", From a8a691357c1c275486cf80658813a8cee8c1aa3a Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Mon, 2 Jan 2017 13:06:13 -0600 Subject: [PATCH 14/54] bump version post-release --- docs/source/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7a86446..c429f46 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,6 @@ master_doc = 'index' project = u'Effect' copyright = u'2015, Christopher Armstrong' -version = release = '0.11.0' +version = release = '0.11.0+' html_theme = 'sphinx_rtd_theme' diff --git a/setup.py b/setup.py index e6812ad..f1f6973 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name="effect", - version="0.11.0", + version="0.11.0+", description="pure effects for Python", long_description=open('README.rst').read(), url="http://github.com/python-effect/effect/", From 26e3d3c89b7066230751eff1b15ba3f72263b639 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Wed, 28 Feb 2018 17:34:50 -0600 Subject: [PATCH 15/54] Define exceptions with @attr.s(hash=True) otherwise they don't work with the Python traceback machinery. --- .gitignore | 1 + Pipfile | 23 ++++ Pipfile.lock | 174 +++++++++++++++++++++++++++++ effect/_intents.py | 2 +- effect/test_parallel_performers.py | 2 +- 5 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/.gitignore b/.gitignore index 949b543..2ce8425 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ effect.egg-info/ dist/ *~ +.pytest_cache diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..51756d2 --- /dev/null +++ b/Pipfile @@ -0,0 +1,23 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[requires] + +python_version = "3.5" + + +[packages] + +six = "~=1.11.0" +attrs = "~=17.4.0" + + +[dev-packages] + +pytest = "~=3.4.1" +testtools = "~=2.3.0" +"flake8" = "~=3.5.0" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..2d4ada7 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,174 @@ +{ + "_meta": { + "hash": { + "sha256": "a00ba453f8383b3875fed17ad2dbdcf9eef1bbff0412602ee0a52fc3f683e302" + }, + "host-environment-markers": { + "implementation_name": "cpython", + "implementation_version": "3.5.2", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "4.4.0-43-Microsoft", + "platform_system": "Linux", + "platform_version": "#1-Microsoft Wed Dec 31 14:42:53 PST 2014", + "python_full_version": "3.5.2", + "python_version": "3.5", + "sys_platform": "linux" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.5" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", + "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" + ], + "version": "==17.4.0" + }, + "six": { + "hashes": [ + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" + ], + "version": "==1.11.0" + } + }, + "develop": { + "argparse": { + "hashes": [ + "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", + "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4" + ], + "version": "==1.4.0" + }, + "attrs": { + "hashes": [ + "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", + "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" + ], + "version": "==17.4.0" + }, + "extras": { + "hashes": [ + "sha256:f689f08df47e2decf76aa6208c081306e7bd472630eb1ec8a875c67de2366e87", + "sha256:132e36de10b9c91d5d4cc620160a476e0468a88f16c9431817a6729611a81b4e" + ], + "version": "==1.0.0" + }, + "fixtures": { + "hashes": [ + "sha256:2a551b0421101de112d9497fb5f6fd25e5019391c0fbec9bad591ecae981420d", + "sha256:fcf0d60234f1544da717a9738325812de1f42c2fa085e2d9252d8fff5712b2ef" + ], + "version": "==3.0.0" + }, + "flake8": { + "hashes": [ + "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37", + "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0" + ], + "version": "==3.5.0" + }, + "linecache2": { + "hashes": [ + "sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef", + "sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c" + ], + "version": "==1.0.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pbr": { + "hashes": [ + "sha256:60c25b7dfd054ef9bb0ae327af949dd4676aa09ac3a9471cdc871d8a9213f9ac", + "sha256:05f61c71aaefc02d8e37c0a3eeb9815ff526ea28b3b76324769e6158d7f95be1" + ], + "version": "==3.1.1" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", + "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" + ], + "version": "==1.5.2" + }, + "pycodestyle": { + "hashes": [ + "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9", + "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766" + ], + "version": "==2.3.1" + }, + "pyflakes": { + "hashes": [ + "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", + "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" + ], + "version": "==1.6.0" + }, + "pytest": { + "hashes": [ + "sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d", + "sha256:9ddcb879c8cc859d2540204b5399011f842e5e8823674bf429f70ada281b3cc6" + ], + "version": "==3.4.1" + }, + "python-mimeparse": { + "hashes": [ + "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282", + "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78" + ], + "version": "==1.6.0" + }, + "six": { + "hashes": [ + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" + ], + "version": "==1.11.0" + }, + "testtools": { + "hashes": [ + "sha256:a2be448869171b6e0f26d9544088b8b98439ec180ce272040236d570a40bcbed", + "sha256:5827ec6cf8233e0f29f51025addd713ca010061204fdea77484a2934690a0559" + ], + "version": "==2.3.0" + }, + "traceback2": { + "hashes": [ + "sha256:8253cebec4b19094d67cc5ed5af99bf1dba1285292226e98a31929f87a5d6b23", + "sha256:05acc67a09980c2ecfedd3423f7ae0104839eccb55fc645773e1caa0951c3030" + ], + "version": "==1.4.0" + }, + "unittest2": { + "hashes": [ + "sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8", + "sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579" + ], + "version": "==1.1.0" + } + } +} diff --git a/effect/_intents.py b/effect/_intents.py index c030b97..dd78243 100644 --- a/effect/_intents.py +++ b/effect/_intents.py @@ -87,7 +87,7 @@ def parallel_all_errors(effects): return Effect(ParallelEffects(list(effects))) -@attr.s +@attr.s(hash=True) class FirstError(Exception): """ One of the effects in a :obj:`ParallelEffects` resulted in an error. This diff --git a/effect/test_parallel_performers.py b/effect/test_parallel_performers.py index 4c5fe39..e428759 100644 --- a/effect/test_parallel_performers.py +++ b/effect/test_parallel_performers.py @@ -12,7 +12,7 @@ from ._test_utils import MatchesReraisedExcInfo, get_exc_info -@attr.s +@attr.s(hash=True) class EquitableException(Exception): message = attr.ib() From 71abc15fd0939d3c6052957047c8b6690a138e23 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Wed, 28 Feb 2018 17:43:08 -0600 Subject: [PATCH 16/54] clean up lints (and disable the bare-except one) --- Makefile | 2 +- effect/fold.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index c8844e0..adeb5d9 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ lint: - flake8 --ignore=E131,E301,E302,E731,W503,E701,E704 --max-line-length=100 effect/ + flake8 --ignore=E131,E301,E302,E731,W503,E701,E704,E722 --max-line-length=100 effect/ build-dist: rm -rf dist diff --git a/effect/fold.py b/effect/fold.py index df9dd05..a54fb4f 100644 --- a/effect/fold.py +++ b/effect/fold.py @@ -70,9 +70,9 @@ def sequence(effects): """ # Could be: folder = lambda acc, el: acc + [el] # But, for peformance: - l = [] + result = [] def folder(acc, el): - l.append(el) - return l - return fold_effect(folder, l, effects) + result.append(el) + return result + return fold_effect(folder, result, effects) From 9946975141af6220ed647baf2ca2367fe9152aaf Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Wed, 28 Feb 2018 21:33:50 -0600 Subject: [PATCH 17/54] add sphinx and sphinx-rtd-theme to Pipfile --- Pipfile | 2 + Pipfile.lock | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 51756d2..98987f7 100644 --- a/Pipfile +++ b/Pipfile @@ -21,3 +21,5 @@ attrs = "~=17.4.0" pytest = "~=3.4.1" testtools = "~=2.3.0" "flake8" = "~=3.5.0" +sphinx = "~=1.7.1" +sphinx-rtd-theme = "~=0.2.4" diff --git a/Pipfile.lock b/Pipfile.lock index 2d4ada7..1423882 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a00ba453f8383b3875fed17ad2dbdcf9eef1bbff0412602ee0a52fc3f683e302" + "sha256": "a012a1175fb4f48d6f73ebe67fe526a2e1bc671ef274588cbfa1b45aab671022" }, "host-environment-markers": { "implementation_name": "cpython", @@ -45,6 +45,13 @@ } }, "develop": { + "alabaster": { + "hashes": [ + "sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732", + "sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0" + ], + "version": "==0.7.10" + }, "argparse": { "hashes": [ "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", @@ -59,6 +66,35 @@ ], "version": "==17.4.0" }, + "babel": { + "hashes": [ + "sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80", + "sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14" + ], + "version": "==2.5.3" + }, + "certifi": { + "hashes": [ + "sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296", + "sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d" + ], + "version": "==2018.1.18" + }, + "chardet": { + "hashes": [ + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae" + ], + "version": "==3.0.4" + }, + "docutils": { + "hashes": [ + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6", + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274" + ], + "version": "==0.14" + }, "extras": { "hashes": [ "sha256:f689f08df47e2decf76aa6208c081306e7bd472630eb1ec8a875c67de2366e87", @@ -80,6 +116,27 @@ ], "version": "==3.5.0" }, + "idna": { + "hashes": [ + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4", + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f" + ], + "version": "==2.6" + }, + "imagesize": { + "hashes": [ + "sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18", + "sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315" + ], + "version": "==1.0.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, "linecache2": { "hashes": [ "sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef", @@ -87,6 +144,12 @@ ], "version": "==1.0.0" }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -94,6 +157,13 @@ ], "version": "==0.6.1" }, + "packaging": { + "hashes": [ + "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", + "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b" + ], + "version": "==17.1" + }, "pbr": { "hashes": [ "sha256:60c25b7dfd054ef9bb0ae327af949dd4676aa09ac3a9471cdc871d8a9213f9ac", @@ -128,6 +198,25 @@ ], "version": "==1.6.0" }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "pyparsing": { + "hashes": [ + "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010", + "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", + "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", + "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", + "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", + "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", + "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58" + ], + "version": "==2.2.0" + }, "pytest": { "hashes": [ "sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d", @@ -142,6 +231,27 @@ ], "version": "==1.6.0" }, + "pytz": { + "hashes": [ + "sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe", + "sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda", + "sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9", + "sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f", + "sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd", + "sha256:3a47ff71597f821cd84a162e71593004286e5be07a340fd462f0d33a760782b5", + "sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d", + "sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef", + "sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0" + ], + "version": "==2018.3" + }, + "requests": { + "hashes": [ + "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", + "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" + ], + "version": "==2.18.4" + }, "six": { "hashes": [ "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", @@ -149,6 +259,34 @@ ], "version": "==1.11.0" }, + "snowballstemmer": { + "hashes": [ + "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89", + "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128" + ], + "version": "==1.2.1" + }, + "sphinx": { + "hashes": [ + "sha256:41ae26acc6130ccf6ed47e5cca73742b80d55a134f0ab897c479bba8d3640b8e", + "sha256:da987de5fcca21a4acc7f67a86a363039e67ac3e8827161e61b91deb131c0ee8" + ], + "version": "==1.7.1" + }, + "sphinx-rtd-theme": { + "hashes": [ + "sha256:62ee4752716e698bad7de8a18906f42d33664128eea06c46b718fc7fbd1a9f5c", + "sha256:2df74b8ff6fae6965c527e97cca6c6c944886aae474b490e17f92adfbe843417" + ], + "version": "==0.2.4" + }, + "sphinxcontrib-websupport": { + "hashes": [ + "sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2", + "sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9" + ], + "version": "==1.0.1" + }, "testtools": { "hashes": [ "sha256:a2be448869171b6e0f26d9544088b8b98439ec180ce272040236d570a40bcbed", @@ -169,6 +307,13 @@ "sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579" ], "version": "==1.1.0" + }, + "urllib3": { + "hashes": [ + "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", + "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" + ], + "version": "==1.22" } } } From 1a9b252c1921f868681654e913bcce4d2ca89eca Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 10:48:14 -0600 Subject: [PATCH 18/54] add sphinx to the dev requirements --- dev-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 70023d4..6bc8db6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,5 @@ testtools flake8 pytest +sphinx +sphinx-rtd-theme From 238a8a2faa76acf7b93179fde99ca03636e6163f Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 10:49:53 -0600 Subject: [PATCH 19/54] rename effect.async to effect.parallel_async, replace async.py with 'from .parallel_async import *' this should hopefully allow us to be backwards compatible so older versions of python can still use effect.async, but to switch to python 3.7 you will need to import effect.parallel_async. --- effect/_base.py | 4 ++-- effect/_intents.py | 2 +- effect/async.py | 42 ++-------------------------------------- effect/parallel_async.py | 40 ++++++++++++++++++++++++++++++++++++++ effect/test_async.py | 2 +- effect/test_intents.py | 2 +- 6 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 effect/parallel_async.py diff --git a/effect/_base.py b/effect/_base.py index b428032..f94d7d8 100644 --- a/effect/_base.py +++ b/effect/_base.py @@ -116,8 +116,8 @@ def perform(dispatcher, effect): .. [#dispatcher] The dispatcher is passed because some performers need to make recursive calls to :func:`perform`, because they need to perform - other effects (see :func:`parallel` and :func:`.perform_parallel_async` - for an example of this). + other effects (see :func:`parallel` and + :func:`.parallel_async.perform_parallel_async` for an example of this). .. [#box] Without using one of those decorators, the performer is actually passed three arguments, not two: the dispatcher, the intent, and a diff --git a/effect/_intents.py b/effect/_intents.py index dd78243..3c353b4 100644 --- a/effect/_intents.py +++ b/effect/_intents.py @@ -26,7 +26,7 @@ class ParallelEffects(object): An effect intent that asks for a number of effects to be run in parallel, and for their results to be gathered up into a sequence. - :func:`effect.async.perform_parallel_async` can perform this Intent + :func:`effect.parallel_async.perform_parallel_async` can perform this Intent assuming all child effects have asynchronous performers. :func:`effect.threads.perform_parallel_with_pool` can perform blocking performers in a thread pool. diff --git a/effect/async.py b/effect/async.py index 32eea71..c8f4295 100644 --- a/effect/async.py +++ b/effect/async.py @@ -1,40 +1,2 @@ -"""Generic asynchronous performers.""" - -from functools import partial -from itertools import count - -from ._base import perform -from ._intents import FirstError - - -def perform_parallel_async(dispatcher, intent, box): - """ - A performer for :obj:`ParallelEffects` which works if all child Effects are - already asynchronous. Use this for things like Twisted, asyncio, etc. - - WARNING: If this is used when child Effects have blocking performers, it - will run them in serial, not parallel. - """ - effects = list(intent.effects) - if not effects: - box.succeed([]) - return - num_results = count() - results = [None] * len(effects) - - def succeed(index, result): - results[index] = result - if next(num_results) + 1 == len(effects): - box.succeed(results) - - def fail(index, result): - box.fail((FirstError, - FirstError(exc_info=result, index=index), - result[2])) - - for index, effect in enumerate(effects): - perform( - dispatcher, - effect.on( - success=partial(succeed, index), - error=partial(fail, index))) +from .parallel_async import * +__all__ = ['perform_parallel_async'] diff --git a/effect/parallel_async.py b/effect/parallel_async.py new file mode 100644 index 0000000..32eea71 --- /dev/null +++ b/effect/parallel_async.py @@ -0,0 +1,40 @@ +"""Generic asynchronous performers.""" + +from functools import partial +from itertools import count + +from ._base import perform +from ._intents import FirstError + + +def perform_parallel_async(dispatcher, intent, box): + """ + A performer for :obj:`ParallelEffects` which works if all child Effects are + already asynchronous. Use this for things like Twisted, asyncio, etc. + + WARNING: If this is used when child Effects have blocking performers, it + will run them in serial, not parallel. + """ + effects = list(intent.effects) + if not effects: + box.succeed([]) + return + num_results = count() + results = [None] * len(effects) + + def succeed(index, result): + results[index] = result + if next(num_results) + 1 == len(effects): + box.succeed(results) + + def fail(index, result): + box.fail((FirstError, + FirstError(exc_info=result, index=index), + result[2])) + + for index, effect in enumerate(effects): + perform( + dispatcher, + effect.on( + success=partial(succeed, index), + error=partial(fail, index))) diff --git a/effect/test_async.py b/effect/test_async.py index 2d04595..898a547 100644 --- a/effect/test_async.py +++ b/effect/test_async.py @@ -5,7 +5,7 @@ from ._base import Effect, perform from ._dispatcher import ComposedDispatcher, TypeDispatcher from ._intents import ParallelEffects, base_dispatcher, parallel -from .async import perform_parallel_async +from .parallel_async import perform_parallel_async from .test_base import func_dispatcher from .test_parallel_performers import ParallelPerformerTestsMixin diff --git a/effect/test_intents.py b/effect/test_intents.py index 2fb2273..749c30a 100644 --- a/effect/test_intents.py +++ b/effect/test_intents.py @@ -21,7 +21,7 @@ ParallelEffects, parallel_all_errors) from ._sync import sync_perform from ._test_utils import MatchesReraisedExcInfo, get_exc_info -from .async import perform_parallel_async +from .parallel_async import perform_parallel_async from .test_parallel_performers import EquitableException From 0af08e7a2c4ec2670d9e34f5346f65382ac7fa91 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 10:50:26 -0600 Subject: [PATCH 20/54] regenerate the documentation; effect.async is now hidden. --- Makefile | 2 +- docs/source/api/effect.async.rst | 7 ------- docs/source/api/effect.parallel_async.rst | 7 +++++++ 3 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 docs/source/api/effect.async.rst create mode 100644 docs/source/api/effect.parallel_async.rst diff --git a/Makefile b/Makefile index adeb5d9..fcc01fb 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ upload-dist: doc: rm -rf docs/build rm -rf docs/source/api - cd docs; sphinx-apidoc -e -o source/api ../effect ../setup.py ../examples ../effect/test_*.py + cd docs; sphinx-apidoc -e -o source/api ../effect ../setup.py ../examples ../effect/test_*.py ../effect/async.py rm docs/source/api/modules.rst rm docs/source/api/effect.rst # can't use sed -i on both linux and mac, so... diff --git a/docs/source/api/effect.async.rst b/docs/source/api/effect.async.rst deleted file mode 100644 index feaf833..0000000 --- a/docs/source/api/effect.async.rst +++ /dev/null @@ -1,7 +0,0 @@ -effect.async module -=================== - -.. automodule:: effect.async - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/effect.parallel_async.rst b/docs/source/api/effect.parallel_async.rst new file mode 100644 index 0000000..b2943df --- /dev/null +++ b/docs/source/api/effect.parallel_async.rst @@ -0,0 +1,7 @@ +effect.parallel\_async module +============================= + +.. automodule:: effect.parallel_async + :members: + :undoc-members: + :show-inheritance: From 47fceb5d7ec3771f524a283b68771dbb3a9aa7e1 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 10:52:59 -0600 Subject: [PATCH 21/54] fix test_yield_non_effect since python 3.7 changed inner function naming --- effect/test_do.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effect/test_do.py b/effect/test_do.py index a7ad88f..92d3eaa 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -70,7 +70,7 @@ def f(): perf(result) assert str(err_info.value).startswith( '@do functions must only yield Effects or results of ' - 'do_return. Got 1 from Date: Sat, 1 Dec 2018 10:56:52 -0600 Subject: [PATCH 22/54] fix test_stop_iteration_only_local to work on Python 3.7 --- effect/test_do.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/effect/test_do.py b/effect/test_do.py index 92d3eaa..a097dd5 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -161,8 +161,13 @@ def f(): yield Effect(Constant('foo')) eff = f() - with raises(StopIteration): - perf(eff) + if sys.version_info > (3, 7): + # In Python 3.7, generators straight up aren't allowed to raise StopIteration any more + with raises(RuntimeError): + perf(eff) + else: + with raises(StopIteration): + perf(eff) @mark.skipif(not six.PY3, reason="Testing a Py3-specific feature") From 4465bc3672cb69e87c05f308f943baad45a61236 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 10:57:49 -0600 Subject: [PATCH 23/54] add python 3.7 to the build matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4eacaa4..e9e8502 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "2.7" - "3.4" - "3.5" + - "3.7" - "pypy" install: - pip install . From 6ee373d7e23ff89c9bfff5f5c8dd5744688adbd0 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 11:15:35 -0600 Subject: [PATCH 24/54] ok I guess I can be explicit about the names imported --- effect/async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effect/async.py b/effect/async.py index c8f4295..e94a6aa 100644 --- a/effect/async.py +++ b/effect/async.py @@ -1,2 +1,2 @@ -from .parallel_async import * +from .parallel_async import perform_parallel_async __all__ = ['perform_parallel_async'] From aef6b18cb121c25ce394b43ef6812dfa1035b2a1 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 11:18:49 -0600 Subject: [PATCH 25/54] apparently travis doesn't support python 3.7 properly yet. this might make it work? --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e9e8502..0886e05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,12 @@ python: - "2.7" - "3.4" - "3.5" - - "3.7" - "pypy" +matrix: + include: + - python: 3.7 + dist: xenial + sudo: true install: - pip install . - pip install -r dev-requirements.txt From 3631361115d24cfbeef6b6ace9a08732d9aba058 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 11:30:30 -0600 Subject: [PATCH 26/54] delete Pipenv stuff. --- Pipfile | 25 ---- Pipfile.lock | 319 --------------------------------------------------- 2 files changed, 344 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 98987f7..0000000 --- a/Pipfile +++ /dev/null @@ -1,25 +0,0 @@ -[[source]] - -url = "https://pypi.python.org/simple" -verify_ssl = true -name = "pypi" - - -[requires] - -python_version = "3.5" - - -[packages] - -six = "~=1.11.0" -attrs = "~=17.4.0" - - -[dev-packages] - -pytest = "~=3.4.1" -testtools = "~=2.3.0" -"flake8" = "~=3.5.0" -sphinx = "~=1.7.1" -sphinx-rtd-theme = "~=0.2.4" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 1423882..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,319 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "a012a1175fb4f48d6f73ebe67fe526a2e1bc671ef274588cbfa1b45aab671022" - }, - "host-environment-markers": { - "implementation_name": "cpython", - "implementation_version": "3.5.2", - "os_name": "posix", - "platform_machine": "x86_64", - "platform_python_implementation": "CPython", - "platform_release": "4.4.0-43-Microsoft", - "platform_system": "Linux", - "platform_version": "#1-Microsoft Wed Dec 31 14:42:53 PST 2014", - "python_full_version": "3.5.2", - "python_version": "3.5", - "sys_platform": "linux" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.5" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "attrs": { - "hashes": [ - "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", - "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" - ], - "version": "==17.4.0" - }, - "six": { - "hashes": [ - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" - ], - "version": "==1.11.0" - } - }, - "develop": { - "alabaster": { - "hashes": [ - "sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732", - "sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0" - ], - "version": "==0.7.10" - }, - "argparse": { - "hashes": [ - "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", - "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4" - ], - "version": "==1.4.0" - }, - "attrs": { - "hashes": [ - "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", - "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" - ], - "version": "==17.4.0" - }, - "babel": { - "hashes": [ - "sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80", - "sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14" - ], - "version": "==2.5.3" - }, - "certifi": { - "hashes": [ - "sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296", - "sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d" - ], - "version": "==2018.1.18" - }, - "chardet": { - "hashes": [ - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae" - ], - "version": "==3.0.4" - }, - "docutils": { - "hashes": [ - "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6", - "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", - "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274" - ], - "version": "==0.14" - }, - "extras": { - "hashes": [ - "sha256:f689f08df47e2decf76aa6208c081306e7bd472630eb1ec8a875c67de2366e87", - "sha256:132e36de10b9c91d5d4cc620160a476e0468a88f16c9431817a6729611a81b4e" - ], - "version": "==1.0.0" - }, - "fixtures": { - "hashes": [ - "sha256:2a551b0421101de112d9497fb5f6fd25e5019391c0fbec9bad591ecae981420d", - "sha256:fcf0d60234f1544da717a9738325812de1f42c2fa085e2d9252d8fff5712b2ef" - ], - "version": "==3.0.0" - }, - "flake8": { - "hashes": [ - "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37", - "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0" - ], - "version": "==3.5.0" - }, - "idna": { - "hashes": [ - "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4", - "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f" - ], - "version": "==2.6" - }, - "imagesize": { - "hashes": [ - "sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18", - "sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315" - ], - "version": "==1.0.0" - }, - "jinja2": { - "hashes": [ - "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", - "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" - ], - "version": "==2.10" - }, - "linecache2": { - "hashes": [ - "sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef", - "sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c" - ], - "version": "==1.0.0" - }, - "markupsafe": { - "hashes": [ - "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" - ], - "version": "==1.0" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "packaging": { - "hashes": [ - "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", - "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b" - ], - "version": "==17.1" - }, - "pbr": { - "hashes": [ - "sha256:60c25b7dfd054ef9bb0ae327af949dd4676aa09ac3a9471cdc871d8a9213f9ac", - "sha256:05f61c71aaefc02d8e37c0a3eeb9815ff526ea28b3b76324769e6158d7f95be1" - ], - "version": "==3.1.1" - }, - "pluggy": { - "hashes": [ - "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" - ], - "version": "==0.6.0" - }, - "py": { - "hashes": [ - "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", - "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" - ], - "version": "==1.5.2" - }, - "pycodestyle": { - "hashes": [ - "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9", - "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766" - ], - "version": "==2.3.1" - }, - "pyflakes": { - "hashes": [ - "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", - "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" - ], - "version": "==1.6.0" - }, - "pygments": { - "hashes": [ - "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", - "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" - ], - "version": "==2.2.0" - }, - "pyparsing": { - "hashes": [ - "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010", - "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", - "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", - "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", - "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", - "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", - "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58" - ], - "version": "==2.2.0" - }, - "pytest": { - "hashes": [ - "sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d", - "sha256:9ddcb879c8cc859d2540204b5399011f842e5e8823674bf429f70ada281b3cc6" - ], - "version": "==3.4.1" - }, - "python-mimeparse": { - "hashes": [ - "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282", - "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78" - ], - "version": "==1.6.0" - }, - "pytz": { - "hashes": [ - "sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe", - "sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda", - "sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9", - "sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f", - "sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd", - "sha256:3a47ff71597f821cd84a162e71593004286e5be07a340fd462f0d33a760782b5", - "sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d", - "sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef", - "sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0" - ], - "version": "==2018.3" - }, - "requests": { - "hashes": [ - "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", - "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" - ], - "version": "==2.18.4" - }, - "six": { - "hashes": [ - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" - ], - "version": "==1.11.0" - }, - "snowballstemmer": { - "hashes": [ - "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89", - "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128" - ], - "version": "==1.2.1" - }, - "sphinx": { - "hashes": [ - "sha256:41ae26acc6130ccf6ed47e5cca73742b80d55a134f0ab897c479bba8d3640b8e", - "sha256:da987de5fcca21a4acc7f67a86a363039e67ac3e8827161e61b91deb131c0ee8" - ], - "version": "==1.7.1" - }, - "sphinx-rtd-theme": { - "hashes": [ - "sha256:62ee4752716e698bad7de8a18906f42d33664128eea06c46b718fc7fbd1a9f5c", - "sha256:2df74b8ff6fae6965c527e97cca6c6c944886aae474b490e17f92adfbe843417" - ], - "version": "==0.2.4" - }, - "sphinxcontrib-websupport": { - "hashes": [ - "sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2", - "sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9" - ], - "version": "==1.0.1" - }, - "testtools": { - "hashes": [ - "sha256:a2be448869171b6e0f26d9544088b8b98439ec180ce272040236d570a40bcbed", - "sha256:5827ec6cf8233e0f29f51025addd713ca010061204fdea77484a2934690a0559" - ], - "version": "==2.3.0" - }, - "traceback2": { - "hashes": [ - "sha256:8253cebec4b19094d67cc5ed5af99bf1dba1285292226e98a31929f87a5d6b23", - "sha256:05acc67a09980c2ecfedd3423f7ae0104839eccb55fc645773e1caa0951c3030" - ], - "version": "==1.4.0" - }, - "unittest2": { - "hashes": [ - "sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8", - "sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579" - ], - "version": "==1.1.0" - }, - "urllib3": { - "hashes": [ - "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", - "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" - ], - "version": "==1.22" - } - } -} From 197531a73c8edda39a918e8889ba382dfe78b3fc Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 11:37:36 -0600 Subject: [PATCH 27/54] bump to v0.12.0 --- docs/source/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index c429f46..010f7c1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,6 @@ master_doc = 'index' project = u'Effect' copyright = u'2015, Christopher Armstrong' -version = release = '0.11.0+' +version = release = '0.12.0' html_theme = 'sphinx_rtd_theme' diff --git a/setup.py b/setup.py index f1f6973..b7d42f4 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name="effect", - version="0.11.0+", + version="0.12.0", description="pure effects for Python", long_description=open('README.rst').read(), url="http://github.com/python-effect/effect/", From c739312d34c5740eb0945c487a0a5e0429cf6359 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 1 Dec 2018 11:38:24 -0600 Subject: [PATCH 28/54] bump version to 0.12.0+ --- docs/source/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 010f7c1..cdac392 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,6 @@ master_doc = 'index' project = u'Effect' copyright = u'2015, Christopher Armstrong' -version = release = '0.12.0' +version = release = '0.12.0+' html_theme = 'sphinx_rtd_theme' diff --git a/setup.py b/setup.py index b7d42f4..7e045a6 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name="effect", - version="0.12.0", + version="0.12.0+", description="pure effects for Python", long_description=open('README.rst').read(), url="http://github.com/python-effect/effect/", From 4ea34a455f81168425eda16b843767027bc00dbf Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:41:04 -0600 Subject: [PATCH 29/54] change effect to use Except instances instead of exc_info tuples. This means that effect now only supports Python 3 and above. --- .vscode/settings.json | 3 ++ docs/source/intro.rst | 2 +- effect/_base.py | 50 +++++++++++++----------------- effect/_intents.py | 6 ++-- effect/_sync.py | 6 ++-- effect/_test_utils.py | 30 ++++++++++-------- effect/do.py | 5 ++- effect/fold.py | 5 ++- effect/parallel_async.py | 4 +-- effect/retry.py | 4 +-- effect/test_base.py | 45 ++++++++++++--------------- effect/test_do.py | 8 ++--- effect/test_fold.py | 8 ++--- effect/test_intents.py | 21 ++++++------- effect/test_parallel_performers.py | 16 +++++----- effect/test_retry.py | 2 +- effect/test_sync.py | 5 +-- effect/test_testing.py | 8 ++--- effect/testing.py | 8 ++--- effect/threads.py | 9 ++---- setup.cfg | 3 ++ setup.py | 2 +- 22 files changed, 119 insertions(+), 131 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..098c693 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "C:\\Users\\radix\\.virtualenvs\\effect\\Scripts\\python.exe" +} \ No newline at end of file diff --git a/docs/source/intro.rst b/docs/source/intro.rst index 4663957..2690a4f 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -47,7 +47,7 @@ the ``on`` method: def greet(): return get_user_name().on( success=lambda r: Effect(Print("Hello,", r)), - error=lambda exc_info: Effect(Print("There was an error!", exc_info[1]))) + error=lambda exc: Effect(Print("There was an error!", exc))) (Here we assume another intent, ``Print``, which shows some text to the user.) diff --git a/effect/_base.py b/effect/_base.py index f94d7d8..b7e3879 100644 --- a/effect/_base.py +++ b/effect/_base.py @@ -2,13 +2,10 @@ from __future__ import print_function, absolute_import import sys - from functools import partial import attr -import six - from ._continuation import trampoline @@ -34,8 +31,7 @@ def on(self, success=None, error=None): The result of the Effect will be passed to the first callback. Any callbacks added afterwards will receive the result of the previous callback. Normal return values are passed on to the next ``success`` - callback, and exceptions are passed to the next ``error`` callback - as a ``sys.exc_info()`` tuple. + callback, and exceptions are passed to the next ``error`` callback. If a callback returns an :obj:`Effect`, the result of that :obj:`Effect` will be passed to the next callback. @@ -62,7 +58,7 @@ def succeed(self, result): def fail(self, result): """ - Indicate that the effect has failed. result must be an exc_info tuple. + Indicate that the effect has failed. result must be an exception. """ self._cont((True, result)) @@ -71,13 +67,13 @@ def guard(f, *args, **kwargs): """ Run a function. - Return (is_error, result), where is_error is a boolean indicating whether - it raised an exception. In that case result will be ``sys.exc_info()``. + Return (is_error, result), where ``is_error`` is a boolean indicating whether + it raised an exception. In that case, ``result`` will be an exception. """ try: return (False, f(*args, **kwargs)) - except: - return (True, sys.exc_info()) + except Exception as e: + return (True, e) class NoPerformerFoundError(Exception): @@ -110,7 +106,7 @@ def perform(dispatcher, effect): or return another Effect, which will be recursively performed, such that the result of the returned Effect becomes the result passed to the next callback. In the case of exceptions, the next error-callback will be called - with a ``sys.exc_info()``-style tuple. + with the exception instance. :returns: None @@ -123,9 +119,8 @@ def perform(dispatcher, effect): passed three arguments, not two: the dispatcher, the intent, and a "box". The box is an object that lets the performer provide the result, optionally asynchronously. To provide the result, use - ``box.succeed(result)`` or ``box.fail(exc_info)``, where ``exc_info`` is - a ``sys.exc_info()``-style tuple. Decorators like :func:`sync_performer` - simply abstract this away. + ``box.succeed(result)`` or ``box.fail(exc)``, where ``exc`` is + an exception. Decorators like :func:`sync_performer` simply abstract this away. """ def _run_callbacks(bouncer, chain, result): is_error, value = result @@ -156,8 +151,7 @@ def _perform(bouncer, effect): effect.intent, _Box(partial(bouncer.bounce, _run_callbacks, effect.callbacks))) - except: - e = sys.exc_info() + except Exception as e: _run_callbacks(bouncer, effect.callbacks, (True, e)) trampoline(_perform, effect) @@ -168,27 +162,25 @@ def catch(exc_type, callable): A helper for handling errors of a specific type:: eff.on(error=catch(SpecificException, - lambda exc_info: "got an error!")) + lambda exc: "got an error!")) If any exception other than a ``SpecificException`` is thrown, it will be ignored by this handler and propogate further down the chain of callbacks. """ - def catcher(exc_info): - if isinstance(exc_info[1], exc_type): - return callable(exc_info) - six.reraise(*exc_info) + def catcher(error): + if isinstance(error, exc_type): + return callable(error) + raise error return catcher -def raise_(exception, tb=None): - """Simple convenience function to allow raising exceptions from lambdas. - - This is slightly more convenient than ``six.reraise`` because it takes an - exception instance instead of needing the type separate from the instance. +def raise_(exception): + """Simple convenience function to allow raising exceptions as an expression, + useful in lambdas. :param exception: An exception *instance* (not an exception type). - - ``raise_(exc)`` is the same as ``raise exc``. - - ``raise_(exc, tb)`` is the same as ``raise type(exc), exc, tb``. + ``raise_(exc)`` is the same as ``raise exc``. """ - six.reraise(type(exception), exception, tb) + raise exception + diff --git a/effect/_intents.py b/effect/_intents.py index 3c353b4..7a0825a 100644 --- a/effect/_intents.py +++ b/effect/_intents.py @@ -77,7 +77,7 @@ def parallel_all_errors(effects): :param effects: Effects which should be performed in parallel. :return: An Effect that results in a list of ``(is_error, result)`` tuples, where ``is_error`` is True if the child effect raised an exception, in - which case ``result`` will be an exc_info tuple. If ``is_error`` is + which case ``result`` will be the exception. If ``is_error`` is False, then ``result`` will just be the result as provided by the child effect. """ @@ -93,12 +93,12 @@ class FirstError(Exception): One of the effects in a :obj:`ParallelEffects` resulted in an error. This represents the first such error that occurred. """ - exc_info = attr.ib() + exception = attr.ib() index = attr.ib() def __str__(self): return '(index=%s) %s: %s' % ( - self.index, self.exc_info[0].__name__, self.exc_info[1]) + self.index, type(self.exception).__name__, self.exception) @attr.s diff --git a/effect/_sync.py b/effect/_sync.py index 2a93d87..31c18ba 100644 --- a/effect/_sync.py +++ b/effect/_sync.py @@ -31,7 +31,7 @@ def sync_perform(dispatcher, effect): if successes: return successes[0] elif errors: - six.reraise(*errors[0]) + raise errors[0] else: raise NotSynchronousError("Performing %r was not synchronous!" % (effect,)) @@ -70,6 +70,6 @@ def sync_wrapper(*args, **kwargs): pass_args = args[:-1] try: box.succeed(f(*pass_args, **kwargs)) - except: - box.fail(sys.exc_info()) + except Exception as e: + box.fail(e) return sync_wrapper diff --git a/effect/_test_utils.py b/effect/_test_utils.py index 51de1d6..9edeec3 100644 --- a/effect/_test_utils.py +++ b/effect/_test_utils.py @@ -5,7 +5,7 @@ import attr -from testtools.matchers import Equals +from testtools.matchers import Equals, Mismatch @attr.s @@ -20,6 +20,18 @@ def describe(self): + ''.join(self.got_tb) + "\nbut it doesn't.") +@attr.s +class MatchesException(object): + expected = attr.ib() + + def match(self, other): + expected_type = type(self.expected) + if type(other) is not expected_type: + return Mismatch('{} is not a {}'.format(other, expected_type)) + if other.args != self.expected.args: + return Mismatch('{} has different arguments: {}.'.format( + other.args, self.expected.args)) + @attr.s class MatchesReraisedExcInfo(object): @@ -27,28 +39,20 @@ class MatchesReraisedExcInfo(object): expected = attr.ib() def match(self, actual): - valcheck = Equals(self.expected[1]).match(actual[1]) + valcheck = Equals(self.expected.args).match(actual.args) if valcheck is not None: return valcheck - typecheck = Equals(self.expected[0]).match(actual[0]) + typecheck = Equals(type(self.expected)).match(type(actual)) if typecheck is not None: return typecheck - expected = traceback.format_exception(*self.expected) - new = traceback.format_exception(*actual) + expected = list(traceback.TracebackException.from_exception(self.expected).format()) + new = list(traceback.TracebackException.from_exception(actual).format()) tail_equals = lambda a, b: a == b[-len(a):] if not tail_equals(expected[1:], new[1:]): return ReraisedTracebackMismatch(expected_tb=expected, got_tb=new) -def get_exc_info(exception): - """Get an exc_info tuple based on an exception instance.""" - try: - raise exception - except: - return sys.exc_info() - - def raise_(e): """Raise an exception instance. Exists so you can raise in a lambda.""" raise e diff --git a/effect/do.py b/effect/do.py index a11bd46..45df5e2 100644 --- a/effect/do.py +++ b/effect/do.py @@ -95,7 +95,7 @@ def foo(): def _do(result, generator, is_error): try: if is_error: - val = generator.throw(*result) + val = generator.throw(result) else: val = generator.send(result) except StopIteration as stop: @@ -104,8 +104,7 @@ def _do(result, generator, is_error): # case where some other code is raising StopIteration up through this # generator, in which case we shouldn't really treat it like a function # return -- it could quite easily hide bugs. - tb = sys.exc_info()[2] - if tb.tb_next: + if stop.__traceback__.tb_next: raise else: # Python 3 allows you to use `return val` in a generator, which diff --git a/effect/fold.py b/effect/fold.py index a54fb4f..bf4c90b 100644 --- a/effect/fold.py +++ b/effect/fold.py @@ -9,15 +9,14 @@ class FoldError(Exception): Raised when one of the Effects passed to :func:`fold_effect` fails. :ivar accumulator: The data accumulated so far, before the failing Effect. - :ivar wrapped_exception: The exc_info tuple representing the original - exception raised by the failing Effect. + :ivar wrapped_exception: The original exception raised by the failing Effect. """ def __init__(self, accumulator, wrapped_exception): self.accumulator = accumulator self.wrapped_exception = wrapped_exception def __str__(self): - tb_lines = traceback.format_exception(*self.wrapped_exception) + tb_lines = traceback.TracebackException.from_exception(self.wrapped_exception).format() tb = ''.join(tb_lines) st = ( " Original traceback follows:\n%s" diff --git a/effect/parallel_async.py b/effect/parallel_async.py index 32eea71..9a02d45 100644 --- a/effect/parallel_async.py +++ b/effect/parallel_async.py @@ -28,9 +28,7 @@ def succeed(index, result): box.succeed(results) def fail(index, result): - box.fail((FirstError, - FirstError(exc_info=result, index=index), - result[2])) + box.fail(FirstError(exception=result, index=index)) for index, effect in enumerate(effects): perform( diff --git a/effect/retry.py b/effect/retry.py index dd87cb7..8640d68 100644 --- a/effect/retry.py +++ b/effect/retry.py @@ -14,7 +14,7 @@ def retry(effect, should_retry): will fail with the most recent error from func. :param effect.Effect effect: Any effect. - :param should_retry: A function which should take an exc_info tuple as an + :param should_retry: A function which should take an exception as an argument and return an effect of bool. """ @@ -22,7 +22,7 @@ def maybe_retry(error, retry_allowed): if retry_allowed: return try_() else: - six.reraise(*error) + raise error def try_(): return effect.on( diff --git a/effect/test_base.py b/effect/test_base.py index 0a5b5c8..fd6a58d 100644 --- a/effect/test_base.py +++ b/effect/test_base.py @@ -4,10 +4,10 @@ import traceback from testtools import TestCase -from testtools.matchers import MatchesException, MatchesListwise +from testtools.matchers import MatchesListwise from ._base import Effect, NoPerformerFoundError, catch, perform -from ._test_utils import raise_ +from ._test_utils import MatchesException, raise_ def func_dispatcher(intent): @@ -75,8 +75,7 @@ def test_error_with_callback(self): the error callback. """ calls = [] - intent = lambda box: box.fail( - (ValueError, ValueError('dispatched'), None)) + intent = lambda box: box.fail(ValueError('dispatched')) perform(func_dispatcher, Effect(intent).on(error=calls.append)) self.assertThat( calls, @@ -85,7 +84,7 @@ def test_error_with_callback(self): def test_dispatcher_raises(self): """ - When a dispatcher raises an exception, the exc_info is passed to the + When a dispatcher raises an exception, the exception is passed to the error handler. """ calls = [] @@ -101,7 +100,7 @@ def test_dispatcher_raises(self): def test_performer_raises(self): """ - When a performer raises an exception, the exc_info is passed to the + When a performer raises an exception, it is passed to the error handler. """ calls = [] @@ -122,8 +121,7 @@ def test_success_propagates_effect_exception(self): the exception is passed to the next callback. """ calls = [] - intent = lambda box: box.fail( - (ValueError, ValueError('dispatched'), None)) + intent = lambda box: box.fail(ValueError('dispatched')) perform(func_dispatcher, Effect(intent) .on(success=lambda box: calls.append("foo")) @@ -146,7 +144,7 @@ def test_error_propagates_effect_result(self): .on(success=calls.append)) self.assertEqual(calls, ["dispatched"]) - def test_callback_sucecss_exception(self): + def test_callback_success_exception(self): """ If a success callback raises an error, the exception is passed to the error callback. @@ -169,8 +167,7 @@ def test_callback_error_exception(self): """ calls = [] - intent = lambda box: box.fail( - (ValueError, ValueError('dispatched'), None)) + intent = lambda box: box.fail(ValueError('dispatched')) perform(func_dispatcher, Effect(intent) @@ -209,11 +206,10 @@ def test_effects_returning_effects_returning_effects(self): def test_nested_effect_exception_passes_to_outer_error_handler(self): """ If an inner effect raises an exception, it bubbles up and the - exc_info is passed to the outer effect's error handlers. + exception is passed to the outer effect's error handlers. """ calls = [] - intent = lambda box: box.fail( - (ValueError, ValueError('oh dear'), None)) + intent = lambda box: box.fail(ValueError('oh dear')) perform(func_dispatcher, Effect(lambda box: box.succeed(Effect(intent))) .on(error=calls.append)) @@ -296,27 +292,26 @@ class CatchTests(TestCase): def test_caught(self): """ - When the exception type matches the type in the ``exc_info`` tuple, the + When the exception type matches the type of the raised exception, the callable is invoked and its result is returned. """ try: raise RuntimeError('foo') - except: - exc_info = sys.exc_info() - result = catch(RuntimeError, lambda e: ('caught', e))(exc_info) - self.assertEqual(result, ('caught', exc_info)) + except Exception as e: + error = e + result = catch(RuntimeError, lambda e: ('caught', e))(error) + self.assertEqual(result, ('caught', error)) def test_missed(self): """ - When the exception type does not match the type in the ``exc_info`` - tuple, the callable is not invoked and the original exception is - reraised. + When the exception type does not match the type of the raised exception, + the callable is not invoked and the original exception is reraised. """ try: raise ZeroDivisionError('foo') - except: - exc_info = sys.exc_info() + except Exception as e: + error = e e = self.assertRaises( ZeroDivisionError, - lambda: catch(RuntimeError, lambda e: ('caught', e))(exc_info)) + lambda: catch(RuntimeError, lambda e: ('caught', e))(error)) self.assertEqual(str(e), 'foo') diff --git a/effect/test_do.py b/effect/test_do.py index a097dd5..a9374bc 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -82,12 +82,12 @@ def test_raise_from_effect(): def f(): try: yield Effect(Error(ZeroDivisionError('foo'))) - except: - got_error = sys.exc_info() + except Exception as e: + got_error = e yield do_return(got_error) - exc_type, exc, _ = perf(f()) - assert exc_type is ZeroDivisionError + exc = perf(f()) + assert type(exc) is ZeroDivisionError assert str(exc) == 'foo' diff --git a/effect/test_fold.py b/effect/test_fold.py index f75d7d0..72a5b15 100644 --- a/effect/test_fold.py +++ b/effect/test_fold.py @@ -2,10 +2,12 @@ import operator from pytest import raises +from testtools.assertions import assert_that from effect import Effect, Error, base_dispatcher, sync_perform from effect.fold import FoldError, fold_effect, sequence from effect.testing import perform_sequence +from ._test_utils import MatchesException def test_fold_effect(): @@ -48,8 +50,7 @@ def test_fold_effect_errors(): with raises(FoldError) as excinfo: perform_sequence(dispatcher, eff) assert excinfo.value.accumulator == 'NilEi' - assert excinfo.value.wrapped_exception[0] is ZeroDivisionError - assert str(excinfo.value.wrapped_exception[1]) == 'foo' + assert_that(excinfo.value.wrapped_exception, MatchesException(ZeroDivisionError('foo'))) def test_fold_effect_str(): @@ -98,5 +99,4 @@ def test_sequence_error(): with raises(FoldError) as excinfo: perform_sequence(dispatcher, eff) assert excinfo.value.accumulator == ['Ei'] - assert excinfo.value.wrapped_exception[0] is ZeroDivisionError - assert str(excinfo.value.wrapped_exception[1]) == 'foo' + assert_that(excinfo.value.wrapped_exception, MatchesException(ZeroDivisionError('foo'))) diff --git a/effect/test_intents.py b/effect/test_intents.py index 749c30a..a7e95d9 100644 --- a/effect/test_intents.py +++ b/effect/test_intents.py @@ -9,7 +9,7 @@ from pytest import raises -from ._base import Effect +from ._base import Effect, raise_ from ._dispatcher import ComposedDispatcher, TypeDispatcher from ._intents import ( base_dispatcher, @@ -20,7 +20,7 @@ FirstError, ParallelEffects, parallel_all_errors) from ._sync import sync_perform -from ._test_utils import MatchesReraisedExcInfo, get_exc_info +from ._test_utils import MatchesReraisedExcInfo from .parallel_async import perform_parallel_async from .test_parallel_performers import EquitableException @@ -59,8 +59,7 @@ def test_perform_func_args_kwargs(): def test_first_error_str(): """FirstErrors have a pleasing format.""" - fe = FirstError(exc_info=(ValueError, ValueError('foo'), None), - index=150) + fe = FirstError(ValueError('foo'), index=150) assert str(fe) == '(index=150) ValueError: foo' @@ -78,13 +77,13 @@ class ParallelAllErrorsTests(TestCase): def test_parallel_all_errors(self): """ - Exceptions raised from child effects get turned into (True, exc_info) + Exceptions raised from child effects get turned into (True, exc) results. """ - exc_info1 = get_exc_info(EquitableException(message='foo')) - reraise1 = partial(six.reraise, *exc_info1) - exc_info2 = get_exc_info(EquitableException(message='bar')) - reraise2 = partial(six.reraise, *exc_info2) + exc1 = EquitableException(message='foo') + reraise1 = partial(raise_, exc1) + exc2 = EquitableException(message='bar') + reraise2 = partial(raise_, exc2) dispatcher = ComposedDispatcher([ TypeDispatcher({ @@ -99,8 +98,8 @@ def test_parallel_all_errors(self): sync_perform(dispatcher, eff), MatchesListwise([ MatchesListwise([Equals(True), - MatchesReraisedExcInfo(exc_info1)]), + MatchesReraisedExcInfo(exc1)]), Equals((False, 1)), MatchesListwise([Equals(True), - MatchesReraisedExcInfo(exc_info2)]), + MatchesReraisedExcInfo(exc2)]), ])) diff --git a/effect/test_parallel_performers.py b/effect/test_parallel_performers.py index e428759..d10610e 100644 --- a/effect/test_parallel_performers.py +++ b/effect/test_parallel_performers.py @@ -6,10 +6,10 @@ from testtools.matchers import MatchesStructure, Equals -from . import Effect +from . import Effect, raise_ from ._intents import Constant, Func, FirstError, parallel from ._sync import sync_perform -from ._test_utils import MatchesReraisedExcInfo, get_exc_info +from ._test_utils import MatchesReraisedExcInfo @attr.s(hash=True) @@ -46,8 +46,8 @@ def test_error(self): When given an effect that results in a Error, ``perform_parallel_async`` result in ``FirstError``. """ - expected_exc_info = get_exc_info(EquitableException(message='foo')) - reraise = partial(six.reraise, *expected_exc_info) + expected_exc = EquitableException(message='foo') + reraise = partial(raise_, expected_exc) try: sync_perform( self.dispatcher, @@ -57,7 +57,7 @@ def test_error(self): fe, MatchesStructure( index=Equals(0), - exc_info=MatchesReraisedExcInfo(expected_exc_info))) + exception=MatchesReraisedExcInfo(expected_exc))) else: self.fail("sync_perform should have raised FirstError.") @@ -66,8 +66,8 @@ def test_error_index(self): The ``index`` of a :obj:`FirstError` is the index of the effect that failed in the list. """ - expected_exc_info = get_exc_info(EquitableException(message='foo')) - reraise = partial(six.reraise, *expected_exc_info) + expected_exc = EquitableException(message='foo') + reraise = partial(raise_, expected_exc) try: sync_perform( self.dispatcher, @@ -80,4 +80,4 @@ def test_error_index(self): fe, MatchesStructure( index=Equals(1), - exc_info=MatchesReraisedExcInfo(expected_exc_info))) + exception=MatchesReraisedExcInfo(expected_exc))) diff --git a/effect/test_retry.py b/effect/test_retry.py index c1cd8bc..e9271be 100644 --- a/effect/test_retry.py +++ b/effect/test_retry.py @@ -53,7 +53,7 @@ def test_continue_retrying(self): lambda: raise_(RuntimeError("3"))) def should_retry(e): - return ESConstant(str(e[1]) != "3") + return ESConstant(str(e) != "3") result = retry(ESFunc(func), should_retry) self.assertThat(lambda: resolve_stubs(base_dispatcher, result), diff --git a/effect/test_sync.py b/effect/test_sync.py index b868dd0..79913ed 100644 --- a/effect/test_sync.py +++ b/effect/test_sync.py @@ -1,10 +1,11 @@ from functools import partial from testtools import TestCase -from testtools.matchers import MatchesException, raises +from testtools.matchers import raises from ._base import Effect from ._sync import NotSynchronousError, sync_perform, sync_performer +from ._test_utils import MatchesException def func_dispatcher(intent): @@ -38,7 +39,7 @@ def test_error_bubbles_up(self): sync_perform. """ def fail(box): - box.fail((ValueError, ValueError("oh dear"), None)) + box.fail(ValueError("oh dear")) self.assertThat( lambda: sync_perform(func_dispatcher, Effect(fail)), raises(ValueError('oh dear'))) diff --git a/effect/test_testing.py b/effect/test_testing.py index bb4e24e..06cf23d 100644 --- a/effect/test_testing.py +++ b/effect/test_testing.py @@ -7,8 +7,7 @@ import pytest from testtools import TestCase -from testtools.matchers import (MatchesListwise, Equals, MatchesException, - raises) +from testtools.matchers import MatchesListwise, Equals, raises from . import ( ComposedDispatcher, @@ -37,6 +36,7 @@ perform_sequence, resolve_effect, resolve_stubs) +from ._test_utils import MatchesException class ResolveEffectTests(TestCase): @@ -123,7 +123,7 @@ def test_resolve_effect_cb_exception(self): 'result'), MatchesListwise([ Equals('handled'), - MatchesException(ZeroDivisionError)])) + MatchesException(ZeroDivisionError('division by zero'))])) def test_raise_if_final_result_is_error(self): """ @@ -466,7 +466,7 @@ def test_parallel_sequence_must_be_parallel(): p = sequence([Effect(1), Effect(2), Effect(3)]) with pytest.raises(FoldError) as excinfo: perform_sequence(seq, p) - assert excinfo.value.wrapped_exception[0] is AssertionError + assert type(excinfo.value.wrapped_exception) is AssertionError def test_nested_sequence(): diff --git a/effect/testing.py b/effect/testing.py index 65dba72..6ac62e8 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -251,7 +251,7 @@ def resolve_effect(effect, result, is_error=False): :param result: If ``is_error`` is False, this can be any object and will be treated as the result of the effect. If ``is_error`` is True, this must - be a three-tuple in the style of ``sys.exc_info``. + be an exception. """ for i, (callback, errback) in enumerate(effect.callbacks): cb = errback if is_error else callback @@ -263,7 +263,7 @@ def resolve_effect(effect, result, is_error=False): result.intent, callbacks=result.callbacks + effect.callbacks[i + 1:]) if is_error: - six.reraise(*result) + raise result return result @@ -273,8 +273,8 @@ def fail_effect(effect, exception): """ try: raise exception - except: - return resolve_effect(effect, sys.exc_info(), is_error=True) + except Exception as e: + return resolve_effect(effect, e, is_error=True) def resolve_stub(dispatcher, effect): diff --git a/effect/threads.py b/effect/threads.py index 1840cb7..4a8bbb7 100644 --- a/effect/threads.py +++ b/effect/threads.py @@ -1,7 +1,5 @@ import sys -import six - from ._intents import FirstError from ._sync import sync_perform, sync_performer @@ -36,9 +34,6 @@ def perform_child(index_and_effect): index, effect = index_and_effect try: return sync_perform(dispatcher, effect) - except: - exc_info = sys.exc_info() - six.reraise(FirstError, - FirstError(exc_info=exc_info, index=index), - exc_info[2]) + except Exception as e: + raise FirstError(exception=e, index=index) return pool.map(perform_child, enumerate(parallel_effects.effects)) diff --git a/setup.cfg b/setup.cfg index 79bc678..c98db00 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ # 3. If at all possible, it is good practice to do this. If you cannot, you # will need to generate wheels for each Python version that you support. universal=1 + +[flake8] +max-line-length = 88 diff --git a/setup.py b/setup.py index 7e045a6..fa10b1e 100644 --- a/setup.py +++ b/setup.py @@ -16,4 +16,4 @@ ], packages=['effect'], install_requires=['six', 'attrs'], - ) +) From 60aa6e01f677d216b0fe3b38265e193d56ada983 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:41:48 -0600 Subject: [PATCH 30/54] remove some references to supporting python 2 --- README.rst | 2 +- setup.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b78eac9..8943933 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ the effects (that is, IO or state manipulation) in your code. Documentation is available at https://effect.readthedocs.org/, and its PyPI page is https://pypi.python.org/pypi/effect. -It `supports`_ Python 2.7, 3.4 and 3.5 as well as PyPy. +It `supports`_ 3.6 and above. .. _`supports`: https://travis-ci.org/python-effect/effect diff --git a/setup.py b/setup.py index fa10b1e..9c2d6ad 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,6 @@ license="MIT", classifiers=[ 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', ], packages=['effect'], From 1cd592ac07c138388a02e8f300cfad1688e18ce3 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:42:05 -0600 Subject: [PATCH 31/54] and another one --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index a6c51f3..f3e656b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,7 +4,7 @@ Effect Effect is a library for helping you write purely functional code by isolating the effects (that is, IO or state manipulation) in your code. -It supports both Python 2.6 and up, and 3.4 and up, as well as PyPy. +It supports both Python 3.6 and up, as well as PyPy. It lives on PyPI at https://pypi.python.org/pypi/effect and GitHub at https://github.com/python-effect/effect. From 86842f79df9d22b9d0b11c71430f235335da0e61 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:43:07 -0600 Subject: [PATCH 32/54] delete vscode settings, woops --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 098c693..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "C:\\Users\\radix\\.virtualenvs\\effect\\Scripts\\python.exe" -} \ No newline at end of file From c0b4da9a399341038076222c70218895e60d55a4 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:44:20 -0600 Subject: [PATCH 33/54] only run travis on 3.6 and up, and drop the special xenial 3.7 thingy --- .travis.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0886e05..1dba02e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,10 @@ sudo: false language: python python: - - "2.7" - - "3.4" - - "3.5" + - "3.6" + - "3.7" + - "3.8" - "pypy" -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true install: - pip install . - pip install -r dev-requirements.txt From bdfe5a6c5c57fc17a01ce9bfe099763f28fec791 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:44:35 -0600 Subject: [PATCH 34/54] pypy3 only --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1dba02e..d76f918 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - "3.6" - "3.7" - "3.8" - - "pypy" + - "pypy3" install: - pip install . - pip install -r dev-requirements.txt From 70851a4351094acc10005992d4a09b3aafe117d4 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:49:37 -0600 Subject: [PATCH 35/54] get rid of all use of the `six` module now that we are py3-only --- effect/_dispatcher.py | 2 -- effect/_sync.py | 1 - effect/io.py | 4 +--- effect/retry.py | 2 -- effect/test_do.py | 3 --- effect/test_intents.py | 2 -- effect/test_io.py | 2 +- effect/test_parallel_performers.py | 2 -- effect/testing.py | 1 - setup.py | 2 +- 10 files changed, 3 insertions(+), 18 deletions(-) diff --git a/effect/_dispatcher.py b/effect/_dispatcher.py index 30b34d4..26e5515 100644 --- a/effect/_dispatcher.py +++ b/effect/_dispatcher.py @@ -4,8 +4,6 @@ import attr -from six.moves import filter - @attr.s class TypeDispatcher(object): diff --git a/effect/_sync.py b/effect/_sync.py index 31c18ba..207b438 100644 --- a/effect/_sync.py +++ b/effect/_sync.py @@ -4,7 +4,6 @@ Tools for dealing with Effects synchronously. """ -import six import sys from ._base import perform diff --git a/effect/io.py b/effect/io.py index b40e733..719a774 100644 --- a/effect/io.py +++ b/effect/io.py @@ -6,7 +6,6 @@ from __future__ import print_function import attr -from six.moves import input from . import sync_performer, TypeDispatcher @@ -32,8 +31,7 @@ def perform_display_print(dispatcher, intent): @sync_performer def perform_get_input_raw_input(dispatcher, intent): """ - Perform a :obj:`Prompt` intent by using ``raw_input`` (or ``input`` on - Python 3). + Perform a :obj:`Prompt` intent by using ``input``. """ return input(intent.prompt) diff --git a/effect/retry.py b/effect/retry.py index 8640d68..217168e 100644 --- a/effect/retry.py +++ b/effect/retry.py @@ -1,7 +1,5 @@ """Retrying effects.""" -import six - from functools import partial diff --git a/effect/test_do.py b/effect/test_do.py index a9374bc..48e5c12 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -4,8 +4,6 @@ from py.test import raises as raises from py.test import mark -import six - from . import ( ComposedDispatcher, Constant, Effect, Error, TypeDispatcher, base_dispatcher, sync_perform, sync_performer) @@ -170,7 +168,6 @@ def f(): perf(eff) -@mark.skipif(not six.PY3, reason="Testing a Py3-specific feature") def test_py3_return(): """The `return x` syntax in Py3 sets the result of the Effect to `x`.""" from effect._test_do_py3 import py3_generator_with_return diff --git a/effect/test_intents.py b/effect/test_intents.py index a7e95d9..07bcf6d 100644 --- a/effect/test_intents.py +++ b/effect/test_intents.py @@ -2,8 +2,6 @@ from functools import partial -import six - from testtools import TestCase from testtools.matchers import Equals, MatchesListwise diff --git a/effect/test_io.py b/effect/test_io.py index 4312717..97ae861 100644 --- a/effect/test_io.py +++ b/effect/test_io.py @@ -13,6 +13,6 @@ def test_perform_display_print(capsys): def test_perform_get_input_raw_input(monkeypatch): """The stdio dispatcher has a performer that reads input.""" monkeypatch.setattr( - 'effect.io.input', + 'builtins.input', lambda p: 'my name' if p == '> ' else 'boo') assert sync_perform(stdio_dispatcher, Effect(Prompt('> '))) == 'my name' diff --git a/effect/test_parallel_performers.py b/effect/test_parallel_performers.py index d10610e..8f90a71 100644 --- a/effect/test_parallel_performers.py +++ b/effect/test_parallel_performers.py @@ -2,8 +2,6 @@ import attr -import six - from testtools.matchers import MatchesStructure, Equals from . import Effect, raise_ diff --git a/effect/testing.py b/effect/testing.py index 6ac62e8..7cfb9e4 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -17,7 +17,6 @@ from ._sync import NotSynchronousError, sync_perform, sync_performer from ._intents import Constant, Error, Func, ParallelEffects, base_dispatcher -import six __all__ = [ 'perform_sequence', diff --git a/setup.py b/setup.py index 9c2d6ad..b7c264a 100644 --- a/setup.py +++ b/setup.py @@ -14,5 +14,5 @@ 'Programming Language :: Python :: 3', ], packages=['effect'], - install_requires=['six', 'attrs'], + install_requires=['attrs'], ) From 8bbd2cc9d840cd91457ac3df1d8885216f94abf2 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:54:05 -0600 Subject: [PATCH 36/54] clean up lint, and also encode flake8 configuration properly --- .travis.yml | 2 +- Makefile | 3 --- effect/_base.py | 2 -- effect/_sync.py | 2 -- effect/_test_utils.py | 3 +-- effect/do.py | 1 - effect/test_base.py | 1 - effect/test_do.py | 3 +-- effect/testing.py | 1 - effect/threads.py | 2 -- setup.cfg | 3 ++- setup.py | 2 +- 12 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index d76f918..6c7c686 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: - pip install -r dev-requirements.txt - pip install sphinx sphinx_rtd_theme script: - - make lint + - flake8 - py.test - make doc diff --git a/Makefile b/Makefile index fcc01fb..9da87da 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,3 @@ -lint: - flake8 --ignore=E131,E301,E302,E731,W503,E701,E704,E722 --max-line-length=100 effect/ - build-dist: rm -rf dist python setup.py sdist bdist_wheel diff --git a/effect/_base.py b/effect/_base.py index b7e3879..d824f7a 100644 --- a/effect/_base.py +++ b/effect/_base.py @@ -1,7 +1,6 @@ # -*- test-case-name: effect.test_base -*- from __future__ import print_function, absolute_import -import sys from functools import partial import attr @@ -183,4 +182,3 @@ def raise_(exception): ``raise_(exc)`` is the same as ``raise exc``. """ raise exception - diff --git a/effect/_sync.py b/effect/_sync.py index 207b438..44a660d 100644 --- a/effect/_sync.py +++ b/effect/_sync.py @@ -4,8 +4,6 @@ Tools for dealing with Effects synchronously. """ -import sys - from ._base import perform from ._utils import wraps diff --git a/effect/_test_utils.py b/effect/_test_utils.py index 9edeec3..56e1dad 100644 --- a/effect/_test_utils.py +++ b/effect/_test_utils.py @@ -1,6 +1,5 @@ """Another sad little utility module.""" -import sys import traceback import attr @@ -30,7 +29,7 @@ def match(self, other): return Mismatch('{} is not a {}'.format(other, expected_type)) if other.args != self.expected.args: return Mismatch('{} has different arguments: {}.'.format( - other.args, self.expected.args)) + other.args, self.expected.args)) @attr.s diff --git a/effect/do.py b/effect/do.py index 45df5e2..e155740 100644 --- a/effect/do.py +++ b/effect/do.py @@ -7,7 +7,6 @@ from __future__ import print_function -import sys import types from . import Effect, Func diff --git a/effect/test_base.py b/effect/test_base.py index fd6a58d..30e3083 100644 --- a/effect/test_base.py +++ b/effect/test_base.py @@ -1,6 +1,5 @@ from __future__ import print_function, absolute_import -import sys import traceback from testtools import TestCase diff --git a/effect/test_do.py b/effect/test_do.py index 48e5c12..9470435 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -1,8 +1,7 @@ import sys from functools import partial -from py.test import raises as raises -from py.test import mark +from py.test import raises from . import ( ComposedDispatcher, Constant, Effect, Error, TypeDispatcher, diff --git a/effect/testing.py b/effect/testing.py index 7cfb9e4..49fee70 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -9,7 +9,6 @@ from contextlib import contextmanager from functools import partial from operator import attrgetter -import sys import attr diff --git a/effect/threads.py b/effect/threads.py index 4a8bbb7..6696043 100644 --- a/effect/threads.py +++ b/effect/threads.py @@ -1,5 +1,3 @@ -import sys - from ._intents import FirstError from ._sync import sync_perform, sync_performer diff --git a/setup.cfg b/setup.cfg index c98db00..846eb4b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,4 +5,5 @@ universal=1 [flake8] -max-line-length = 88 +max-line-length = 100 +ignore = E131,E301,E302,E731,W503,E701,E704,E722 diff --git a/setup.py b/setup.py index b7c264a..a0b09ca 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', - ], + ], packages=['effect'], install_requires=['attrs'], ) From e0551989f0db0156cf7f88c972f21a885a819cf5 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 10:55:52 -0600 Subject: [PATCH 37/54] also remove all __future__ usage --- README.rst | 1 - effect/__init__.py | 2 -- effect/_base.py | 2 -- effect/_intents.py | 3 --- effect/do.py | 3 --- effect/io.py | 1 - effect/test_async.py | 2 -- effect/test_base.py | 2 -- effect/test_intents.py | 2 -- effect/test_threads.py | 2 -- effect/testing.py | 2 -- 11 files changed, 22 deletions(-) diff --git a/README.rst b/README.rst index 8943933..6b4f244 100644 --- a/README.rst +++ b/README.rst @@ -44,7 +44,6 @@ A very quick example of using Effects: .. code:: python - from __future__ import print_function from effect import sync_perform, sync_performer, Effect, TypeDispatcher class ReadLine(object): diff --git a/effect/__init__.py b/effect/__init__.py index 772106d..3e4462c 100644 --- a/effect/__init__.py +++ b/effect/__init__.py @@ -7,8 +7,6 @@ See https://effect.readthedocs.org/ for documentation. """ -from __future__ import absolute_import - from ._base import Effect, perform, NoPerformerFoundError, catch, raise_ from ._sync import ( NotSynchronousError, diff --git a/effect/_base.py b/effect/_base.py index d824f7a..ab9bffb 100644 --- a/effect/_base.py +++ b/effect/_base.py @@ -1,6 +1,4 @@ # -*- test-case-name: effect.test_base -*- -from __future__ import print_function, absolute_import - from functools import partial import attr diff --git a/effect/_intents.py b/effect/_intents.py index 7a0825a..4fc71c2 100644 --- a/effect/_intents.py +++ b/effect/_intents.py @@ -8,9 +8,6 @@ performed, sunch as :class:`Func`, :class:`Error`, and :class:`Constant`. """ - -from __future__ import print_function, absolute_import - import time import attr diff --git a/effect/do.py b/effect/do.py index e155740..66b83f9 100644 --- a/effect/do.py +++ b/effect/do.py @@ -4,9 +4,6 @@ See :func:`do`. """ - -from __future__ import print_function - import types from . import Effect, Func diff --git a/effect/io.py b/effect/io.py index 719a774..ed5dca4 100644 --- a/effect/io.py +++ b/effect/io.py @@ -4,7 +4,6 @@ :obj:`Prompt` that uses built-in Python standard io facilities. """ -from __future__ import print_function import attr from . import sync_performer, TypeDispatcher diff --git a/effect/test_async.py b/effect/test_async.py index 898a547..8d6d96d 100644 --- a/effect/test_async.py +++ b/effect/test_async.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from testtools.testcase import TestCase from ._base import Effect, perform diff --git a/effect/test_base.py b/effect/test_base.py index 30e3083..16cc6f6 100644 --- a/effect/test_base.py +++ b/effect/test_base.py @@ -1,5 +1,3 @@ -from __future__ import print_function, absolute_import - import traceback from testtools import TestCase diff --git a/effect/test_intents.py b/effect/test_intents.py index 07bcf6d..5216ffd 100644 --- a/effect/test_intents.py +++ b/effect/test_intents.py @@ -1,5 +1,3 @@ -from __future__ import print_function, absolute_import - from functools import partial from testtools import TestCase diff --git a/effect/test_threads.py b/effect/test_threads.py index f02b231..4033e30 100644 --- a/effect/test_threads.py +++ b/effect/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from functools import partial from multiprocessing.pool import ThreadPool diff --git a/effect/testing.py b/effect/testing.py index 49fee70..c978ca4 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -4,8 +4,6 @@ Usually the best way to test effects is by using :func:`perform_sequence`. """ -from __future__ import print_function - from contextlib import contextmanager from functools import partial from operator import attrgetter From 3b7aeba6afeeaef5f7ffe4982369ce74659e8051 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 11:16:17 -0600 Subject: [PATCH 38/54] delete the `async.py` module, which was only around for compatibility with old users of effect that were using python 2 --- effect/async.py | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 effect/async.py diff --git a/effect/async.py b/effect/async.py deleted file mode 100644 index e94a6aa..0000000 --- a/effect/async.py +++ /dev/null @@ -1,2 +0,0 @@ -from .parallel_async import perform_parallel_async -__all__ = ['perform_parallel_async'] From e9007da43bfe3e5a0d052d2269dab75af948f3fc Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:01:20 -0600 Subject: [PATCH 39/54] use black to format python code, and update travis to run black --check --- .travis.yml | 3 +- docs/source/conf.py | 18 +-- effect/__init__.py | 45 ++++-- effect/_base.py | 15 +- effect/_continuation.py | 3 +- effect/_dispatcher.py | 1 + effect/_intents.py | 27 ++-- effect/_sync.py | 5 +- effect/_test_utils.py | 29 ++-- effect/_utils.py | 2 + effect/do.py | 22 +-- effect/fold.py | 22 ++- effect/io.py | 9 +- effect/parallel_async.py | 5 +- effect/ref.py | 15 +- effect/retry.py | 4 +- effect/test_async.py | 35 +++-- effect/test_base.py | 170 +++++++++++---------- effect/test_do.py | 66 +++++--- effect/test_fold.py | 56 +++---- effect/test_intents.py | 67 +++++---- effect/test_io.py | 10 +- effect/test_parallel_performers.py | 37 +++-- effect/test_ref.py | 14 +- effect/test_retry.py | 25 ++-- effect/test_sync.py | 47 +++--- effect/test_testing.py | 232 ++++++++++++++++------------- effect/test_threads.py | 6 +- effect/testing.py | 106 +++++++------ effect/threads.py | 1 + setup.py | 10 +- 31 files changed, 628 insertions(+), 479 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c7c686..7ee2267 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,8 @@ install: - pip install sphinx sphinx_rtd_theme script: - flake8 - - py.test + - black --check . + - pytest - make doc notifications: diff --git a/docs/source/conf.py b/docs/source/conf.py index cdac392..af4f3d9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", ] # include __init__ docstrings in class docstrings -autoclass_content = 'both' +autoclass_content = "both" -source_suffix = '.rst' -master_doc = 'index' -project = u'Effect' -copyright = u'2015, Christopher Armstrong' -version = release = '0.12.0+' +source_suffix = ".rst" +master_doc = "index" +project = u"Effect" +copyright = u"2015, Christopher Armstrong" +version = release = "0.12.0+" -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" diff --git a/effect/__init__.py b/effect/__init__.py index 3e4462c..b03abfd 100644 --- a/effect/__init__.py +++ b/effect/__init__.py @@ -8,27 +8,42 @@ """ from ._base import Effect, perform, NoPerformerFoundError, catch, raise_ -from ._sync import ( - NotSynchronousError, - sync_perform, - sync_performer) +from ._sync import NotSynchronousError, sync_perform, sync_performer from ._intents import ( - Delay, perform_delay_with_sleep, - ParallelEffects, parallel, parallel_all_errors, FirstError, - Constant, Error, Func, - base_dispatcher) + Delay, + perform_delay_with_sleep, + ParallelEffects, + parallel, + parallel_all_errors, + FirstError, + Constant, + Error, + Func, + base_dispatcher, +) from ._dispatcher import ComposedDispatcher, TypeDispatcher __all__ = [ # Order here affects the order that these things show up in the API docs. - "Effect", "sync_perform", "sync_performer", + "Effect", + "sync_perform", + "sync_performer", "base_dispatcher", - "TypeDispatcher", "ComposedDispatcher", - "Delay", "perform_delay_with_sleep", - "ParallelEffects", "parallel", "parallel_all_errors", - "Constant", "Error", "Func", - "catch", "raise_", - "NoPerformerFoundError", "NotSynchronousError", "perform", + "TypeDispatcher", + "ComposedDispatcher", + "Delay", + "perform_delay_with_sleep", + "ParallelEffects", + "parallel", + "parallel_all_errors", + "Constant", + "Error", + "Func", + "catch", + "raise_", + "NoPerformerFoundError", + "NotSynchronousError", + "perform", "FirstError", ] diff --git a/effect/_base.py b/effect/_base.py index ab9bffb..6a4f145 100644 --- a/effect/_base.py +++ b/effect/_base.py @@ -33,14 +33,14 @@ def on(self, success=None, error=None): If a callback returns an :obj:`Effect`, the result of that :obj:`Effect` will be passed to the next callback. """ - return Effect(self.intent, - callbacks=self.callbacks + [(success, error)]) + return Effect(self.intent, callbacks=self.callbacks + [(success, error)]) class _Box(object): """ An object into which an effect dispatcher can place a result. """ + def __init__(self, cont): """ :param callable cont: Called with (bool is_error, result) @@ -119,13 +119,14 @@ def perform(dispatcher, effect): ``box.succeed(result)`` or ``box.fail(exc)``, where ``exc`` is an exception. Decorators like :func:`sync_performer` simply abstract this away. """ + def _run_callbacks(bouncer, chain, result): is_error, value = result if type(value) is Effect: bouncer.bounce( - _perform, - Effect(value.intent, callbacks=value.callbacks + chain)) + _perform, Effect(value.intent, callbacks=value.callbacks + chain) + ) return if not chain: @@ -146,8 +147,8 @@ def _perform(bouncer, effect): performer( dispatcher, effect.intent, - _Box(partial(bouncer.bounce, - _run_callbacks, effect.callbacks))) + _Box(partial(bouncer.bounce, _run_callbacks, effect.callbacks)), + ) except Exception as e: _run_callbacks(bouncer, effect.callbacks, (True, e)) @@ -164,10 +165,12 @@ def catch(exc_type, callable): If any exception other than a ``SpecificException`` is thrown, it will be ignored by this handler and propogate further down the chain of callbacks. """ + def catcher(error): if isinstance(error, exc_type): return callable(error) raise error + return catcher diff --git a/effect/_continuation.py b/effect/_continuation.py index b53f1e2..042303c 100644 --- a/effect/_continuation.py +++ b/effect/_continuation.py @@ -19,7 +19,8 @@ def bounce(self, func, *args, **kwargs): if self.work is not None: raise RuntimeError( "Already specified work %r, refusing to set to (%r %r %r)" - % (self.work, func, args, kwargs)) + % (self.work, func, args, kwargs) + ) self.work = (func, args, kwargs) if self._asynchronous: trampoline(func, *args, **kwargs) diff --git a/effect/_dispatcher.py b/effect/_dispatcher.py index 26e5515..d80780b 100644 --- a/effect/_dispatcher.py +++ b/effect/_dispatcher.py @@ -12,6 +12,7 @@ class TypeDispatcher(object): :param mapping: mapping of intent type to performer """ + mapping = attr.ib() def __call__(self, intent): diff --git a/effect/_intents.py b/effect/_intents.py index 4fc71c2..3b38efe 100644 --- a/effect/_intents.py +++ b/effect/_intents.py @@ -78,9 +78,10 @@ def parallel_all_errors(effects): False, then ``result`` will just be the result as provided by the child effect. """ - effects = [effect.on(success=lambda r: (False, r), - error=lambda e: (True, e)) - for effect in effects] + effects = [ + effect.on(success=lambda r: (False, r), error=lambda e: (True, e)) + for effect in effects + ] return Effect(ParallelEffects(list(effects))) @@ -90,12 +91,16 @@ class FirstError(Exception): One of the effects in a :obj:`ParallelEffects` resulted in an error. This represents the first such error that occurred. """ + exception = attr.ib() index = attr.ib() def __str__(self): - return '(index=%s) %s: %s' % ( - self.index, type(self.exception).__name__, self.exception) + return "(index=%s) %s: %s" % ( + self.index, + type(self.exception).__name__, + self.exception, + ) @attr.s @@ -108,6 +113,7 @@ class Delay(object): :param float delay: The number of seconds to delay. """ + delay = attr.ib() @@ -124,6 +130,7 @@ class Constant(object): :param result: The object which the Effect will result in. """ + result = attr.ib() @@ -140,6 +147,7 @@ class Error(object): :param BaseException exception: Exception instance to raise. """ + exception = attr.ib() @@ -173,6 +181,7 @@ class Func(object): :param args: Positional arguments to pass to the function. :param kwargs: Keyword arguments to pass to the function. """ + func = attr.ib() args = attr.ib() kwargs = attr.ib() @@ -189,8 +198,6 @@ def perform_func(dispatcher, intent): return intent.func(*intent.args, **intent.kwargs) -base_dispatcher = TypeDispatcher({ - Constant: perform_constant, - Error: perform_error, - Func: perform_func, -}) +base_dispatcher = TypeDispatcher( + {Constant: perform_constant, Error: perform_error, Func: perform_func,} +) diff --git a/effect/_sync.py b/effect/_sync.py index 44a660d..f7a8cdd 100644 --- a/effect/_sync.py +++ b/effect/_sync.py @@ -30,8 +30,7 @@ def sync_perform(dispatcher, effect): elif errors: raise errors[0] else: - raise NotSynchronousError("Performing %r was not synchronous!" - % (effect,)) + raise NotSynchronousError("Performing %r was not synchronous!" % (effect,)) def sync_performer(f): @@ -61,6 +60,7 @@ def sync_performer(f): def perform_foo(dispatcher, foo): return do_side_effect(foo) """ + @wraps(f) def sync_wrapper(*args, **kwargs): box = args[-1] @@ -69,4 +69,5 @@ def sync_wrapper(*args, **kwargs): box.succeed(f(*pass_args, **kwargs)) except Exception as e: box.fail(e) + return sync_wrapper diff --git a/effect/_test_utils.py b/effect/_test_utils.py index 56e1dad..46ea60f 100644 --- a/effect/_test_utils.py +++ b/effect/_test_utils.py @@ -13,11 +13,14 @@ class ReraisedTracebackMismatch(object): got_tb = attr.ib() def describe(self): - return ("The reference traceback:\n" - + ''.join(self.expected_tb) - + "\nshould match the tail end of the received traceback:\n" - + ''.join(self.got_tb) - + "\nbut it doesn't.") + return ( + "The reference traceback:\n" + + "".join(self.expected_tb) + + "\nshould match the tail end of the received traceback:\n" + + "".join(self.got_tb) + + "\nbut it doesn't." + ) + @attr.s class MatchesException(object): @@ -26,10 +29,11 @@ class MatchesException(object): def match(self, other): expected_type = type(self.expected) if type(other) is not expected_type: - return Mismatch('{} is not a {}'.format(other, expected_type)) + return Mismatch("{} is not a {}".format(other, expected_type)) if other.args != self.expected.args: - return Mismatch('{} has different arguments: {}.'.format( - other.args, self.expected.args)) + return Mismatch( + "{} has different arguments: {}.".format(other.args, self.expected.args) + ) @attr.s @@ -44,12 +48,13 @@ def match(self, actual): typecheck = Equals(type(self.expected)).match(type(actual)) if typecheck is not None: return typecheck - expected = list(traceback.TracebackException.from_exception(self.expected).format()) + expected = list( + traceback.TracebackException.from_exception(self.expected).format() + ) new = list(traceback.TracebackException.from_exception(actual).format()) - tail_equals = lambda a, b: a == b[-len(a):] + tail_equals = lambda a, b: a == b[-len(a) :] if not tail_equals(expected[1:], new[1:]): - return ReraisedTracebackMismatch(expected_tb=expected, - got_tb=new) + return ReraisedTracebackMismatch(expected_tb=expected, got_tb=new) def raise_(e): diff --git a/effect/_utils.py b/effect/_utils.py index 90de164..a5d96e8 100644 --- a/effect/_utils.py +++ b/effect/_utils.py @@ -10,6 +10,7 @@ def wraps(original): This is like :func:`functools.wraps`, except you can wrap non-functions without blowing up. """ + def wraps_decorator(wrapper): try: wrapper.__name__ = original.__name__ @@ -19,4 +20,5 @@ def wraps_decorator(wrapper): except: pass return wrapper + return wraps_decorator diff --git a/effect/do.py b/effect/do.py index 66b83f9..966de3b 100644 --- a/effect/do.py +++ b/effect/do.py @@ -48,22 +48,23 @@ def foo(): (This decorator is named for Haskell's ``do`` notation, which is similar in spirit). """ + @wraps(f) def do_wrapper(*args, **kwargs): - def doit(): gen = f(*args, **kwargs) if not isinstance(gen, types.GeneratorType): raise TypeError( - "%r is not a generator function. It returned %r." - % (f, gen)) + "%r is not a generator function. It returned %r." % (f, gen) + ) return _do(None, gen, False) - fname = getattr(f, '__name__', None) + fname = getattr(f, "__name__", None) if fname is not None: - doit.__name__ = 'do_' + fname + doit.__name__ = "do_" + fname return Effect(Func(doit)) + return do_wrapper @@ -108,13 +109,16 @@ def _do(result, generator, is_error): # set to the return value. So we'll return that value as the # ultimate result of the effect. Python 2 doesn't have the 'value' # attribute of StopIteration, so we'll fall back to None. - return getattr(stop, 'value', None) + return getattr(stop, "value", None) if type(val) is _ReturnSentinel: return val.result elif type(val) is Effect: - return val.on(success=lambda r: _do(r, generator, False), - error=lambda e: _do(e, generator, True)) + return val.on( + success=lambda r: _do(r, generator, False), + error=lambda e: _do(e, generator, True), + ) else: raise TypeError( "@do functions must only yield Effects or results of do_return. " - "Got %r from %r" % (val, generator)) + "Got %r from %r" % (val, generator) + ) diff --git a/effect/fold.py b/effect/fold.py index bf4c90b..65aa6ae 100644 --- a/effect/fold.py +++ b/effect/fold.py @@ -11,17 +11,21 @@ class FoldError(Exception): :ivar accumulator: The data accumulated so far, before the failing Effect. :ivar wrapped_exception: The original exception raised by the failing Effect. """ + def __init__(self, accumulator, wrapped_exception): self.accumulator = accumulator self.wrapped_exception = wrapped_exception def __str__(self): - tb_lines = traceback.TracebackException.from_exception(self.wrapped_exception).format() - tb = ''.join(tb_lines) - st = ( - " Original traceback follows:\n%s" - % (self.accumulator, tb)) - return st.rstrip('\n') + tb_lines = traceback.TracebackException.from_exception( + self.wrapped_exception + ).format() + tb = "".join(tb_lines) + st = " Original traceback follows:\n%s" % ( + self.accumulator, + tb, + ) + return st.rstrip("\n") def fold_effect(f, initial, effects): @@ -54,8 +58,9 @@ def failed(acc, e): raise FoldError(acc, e) def folder(acc, element): - return acc.on(lambda r: element.on(lambda r2: f(r, r2), - error=lambda e: failed(r, e))) + return acc.on( + lambda r: element.on(lambda r2: f(r, r2), error=lambda e: failed(r, e)) + ) return reduce(folder, effects, Effect(Constant(initial))) @@ -74,4 +79,5 @@ def sequence(effects): def folder(acc, el): result.append(el) return result + return fold_effect(folder, result, effects) diff --git a/effect/io.py b/effect/io.py index ed5dca4..d6c127a 100644 --- a/effect/io.py +++ b/effect/io.py @@ -12,12 +12,14 @@ @attr.s class Display(object): """Display some text to the user.""" + output = attr.ib() @attr.s class Prompt(object): """Get some input from the user, with a prompt.""" + prompt = attr.ib() @@ -35,7 +37,6 @@ def perform_get_input_raw_input(dispatcher, intent): return input(intent.prompt) -stdio_dispatcher = TypeDispatcher({ - Display: perform_display_print, - Prompt: perform_get_input_raw_input, -}) +stdio_dispatcher = TypeDispatcher( + {Display: perform_display_print, Prompt: perform_get_input_raw_input,} +) diff --git a/effect/parallel_async.py b/effect/parallel_async.py index 9a02d45..c044ee7 100644 --- a/effect/parallel_async.py +++ b/effect/parallel_async.py @@ -33,6 +33,5 @@ def fail(index, result): for index, effect in enumerate(effects): perform( dispatcher, - effect.on( - success=partial(succeed, index), - error=partial(fail, index))) + effect.on(success=partial(succeed, index), error=partial(fail, index)), + ) diff --git a/effect/ref.py b/effect/ref.py index 2d0350a..f4df760 100644 --- a/effect/ref.py +++ b/effect/ref.py @@ -5,8 +5,13 @@ from ._sync import sync_performer __all__ = [ - 'Reference', 'ReadReference', 'ModifyReference', 'perform_read_reference', - 'perform_modify_reference', 'reference_dispatcher'] + "Reference", + "ReadReference", + "ModifyReference", + "perform_read_reference", + "perform_modify_reference", + "reference_dispatcher", +] class Reference(object): @@ -93,6 +98,6 @@ def perform_modify_reference(dispatcher, intent): return new_value -reference_dispatcher = TypeDispatcher({ - ReadReference: perform_read_reference, - ModifyReference: perform_modify_reference}) +reference_dispatcher = TypeDispatcher( + {ReadReference: perform_read_reference, ModifyReference: perform_modify_reference} +) diff --git a/effect/retry.py b/effect/retry.py index 217168e..f3c1984 100644 --- a/effect/retry.py +++ b/effect/retry.py @@ -24,7 +24,7 @@ def maybe_retry(error, retry_allowed): def try_(): return effect.on( - error=lambda e: should_retry(e).on( - success=partial(maybe_retry, e))) + error=lambda e: should_retry(e).on(success=partial(maybe_retry, e)) + ) return try_() diff --git a/effect/test_async.py b/effect/test_async.py index 8d6d96d..d57dda7 100644 --- a/effect/test_async.py +++ b/effect/test_async.py @@ -13,9 +13,9 @@ class PerformParallelAsyncTests(TestCase, ParallelPerformerTestsMixin): def setUp(self): super(PerformParallelAsyncTests, self).setUp() - self.dispatcher = ComposedDispatcher([ - base_dispatcher, - TypeDispatcher({ParallelEffects: perform_parallel_async})]) + self.dispatcher = ComposedDispatcher( + [base_dispatcher, TypeDispatcher({ParallelEffects: perform_parallel_async})] + ) def test_out_of_order(self): """ @@ -25,17 +25,22 @@ def test_out_of_order(self): """ result = [] boxes = [None] * 2 - eff = parallel([ - Effect(lambda box: boxes.__setitem__(0, box)), - Effect(lambda box: boxes.__setitem__(1, box)), - ]) + eff = parallel( + [ + Effect(lambda box: boxes.__setitem__(0, box)), + Effect(lambda box: boxes.__setitem__(1, box)), + ] + ) perform( - ComposedDispatcher([ - TypeDispatcher({ParallelEffects: perform_parallel_async}), - func_dispatcher, - ]), - eff.on(success=result.append, error=print)) - boxes[1].succeed('a') + ComposedDispatcher( + [ + TypeDispatcher({ParallelEffects: perform_parallel_async}), + func_dispatcher, + ] + ), + eff.on(success=result.append, error=print), + ) + boxes[1].succeed("a") self.assertEqual(result, []) - boxes[0].succeed('b') - self.assertEqual(result[0], ['b', 'a']) + boxes[0].succeed("b") + self.assertEqual(result[0], ["b", "a"]) diff --git a/effect/test_base.py b/effect/test_base.py index 16cc6f6..68acc15 100644 --- a/effect/test_base.py +++ b/effect/test_base.py @@ -12,8 +12,10 @@ def func_dispatcher(intent): Simple effect dispatcher that takes callables taking a box, and calls them with the given box. """ + def performer(dispatcher, intent, box): intent(box) + return performer @@ -31,10 +33,8 @@ def test_no_performer(self): eff = Effect(intent).on(error=calls.append) perform(dispatcher, eff) self.assertThat( - calls, - MatchesListwise([ - MatchesException(NoPerformerFoundError(intent)) - ])) + calls, MatchesListwise([MatchesException(NoPerformerFoundError(intent))]) + ) def test_dispatcher(self): """ @@ -62,9 +62,9 @@ def test_success_with_callback(self): effect's callback """ calls = [] - intent = lambda box: box.succeed('dispatched') + intent = lambda box: box.succeed("dispatched") perform(func_dispatcher, Effect(intent).on(calls.append)) - self.assertEqual(calls, ['dispatched']) + self.assertEqual(calls, ["dispatched"]) def test_error_with_callback(self): """ @@ -72,12 +72,11 @@ def test_error_with_callback(self): the error callback. """ calls = [] - intent = lambda box: box.fail(ValueError('dispatched')) + intent = lambda box: box.fail(ValueError("dispatched")) perform(func_dispatcher, Effect(intent).on(error=calls.append)) self.assertThat( - calls, - MatchesListwise([ - MatchesException(ValueError('dispatched'))])) + calls, MatchesListwise([MatchesException(ValueError("dispatched"))]) + ) def test_dispatcher_raises(self): """ @@ -85,14 +84,11 @@ def test_dispatcher_raises(self): error handler. """ calls = [] - eff = Effect('meaningless').on(error=calls.append) - dispatcher = lambda i: raise_(ValueError('oh dear')) + eff = Effect("meaningless").on(error=calls.append) + dispatcher = lambda i: raise_(ValueError("oh dear")) perform(dispatcher, eff) self.assertThat( - calls, - MatchesListwise([ - MatchesException(ValueError('oh dear')) - ]) + calls, MatchesListwise([MatchesException(ValueError("oh dear"))]) ) def test_performer_raises(self): @@ -101,15 +97,12 @@ def test_performer_raises(self): error handler. """ calls = [] - eff = Effect('meaningless').on(error=calls.append) - performer = lambda d, i, box: raise_(ValueError('oh dear')) + eff = Effect("meaningless").on(error=calls.append) + performer = lambda d, i, box: raise_(ValueError("oh dear")) dispatcher = lambda i: performer perform(dispatcher, eff) self.assertThat( - calls, - MatchesListwise([ - MatchesException(ValueError('oh dear')) - ]) + calls, MatchesListwise([MatchesException(ValueError("oh dear"))]) ) def test_success_propagates_effect_exception(self): @@ -118,15 +111,16 @@ def test_success_propagates_effect_exception(self): the exception is passed to the next callback. """ calls = [] - intent = lambda box: box.fail(ValueError('dispatched')) - perform(func_dispatcher, - Effect(intent) - .on(success=lambda box: calls.append("foo")) - .on(error=calls.append)) + intent = lambda box: box.fail(ValueError("dispatched")) + perform( + func_dispatcher, + Effect(intent) + .on(success=lambda box: calls.append("foo")) + .on(error=calls.append), + ) self.assertThat( - calls, - MatchesListwise([ - MatchesException(ValueError('dispatched'))])) + calls, MatchesListwise([MatchesException(ValueError("dispatched"))]) + ) def test_error_propagates_effect_result(self): """ @@ -135,10 +129,12 @@ def test_error_propagates_effect_result(self): """ calls = [] intent = lambda box: box.succeed("dispatched") - perform(func_dispatcher, - Effect(intent) - .on(error=lambda box: calls.append("foo")) - .on(success=calls.append)) + perform( + func_dispatcher, + Effect(intent) + .on(error=lambda box: calls.append("foo")) + .on(success=calls.append), + ) self.assertEqual(calls, ["dispatched"]) def test_callback_success_exception(self): @@ -148,14 +144,15 @@ def test_callback_success_exception(self): """ calls = [] - perform(func_dispatcher, - Effect(lambda box: box.succeed("foo")) - .on(success=lambda _: raise_(ValueError("oh dear"))) - .on(error=calls.append)) + perform( + func_dispatcher, + Effect(lambda box: box.succeed("foo")) + .on(success=lambda _: raise_(ValueError("oh dear"))) + .on(error=calls.append), + ) self.assertThat( - calls, - MatchesListwise([ - MatchesException(ValueError('oh dear'))])) + calls, MatchesListwise([MatchesException(ValueError("oh dear"))]) + ) def test_callback_error_exception(self): """ @@ -164,16 +161,17 @@ def test_callback_error_exception(self): """ calls = [] - intent = lambda box: box.fail(ValueError('dispatched')) + intent = lambda box: box.fail(ValueError("dispatched")) - perform(func_dispatcher, - Effect(intent) - .on(error=lambda _: raise_(ValueError("oh dear"))) - .on(error=calls.append)) + perform( + func_dispatcher, + Effect(intent) + .on(error=lambda _: raise_(ValueError("oh dear"))) + .on(error=calls.append), + ) self.assertThat( - calls, - MatchesListwise([ - MatchesException(ValueError('oh dear'))])) + calls, MatchesListwise([MatchesException(ValueError("oh dear"))]) + ) def test_effects_returning_effects(self): """ @@ -182,10 +180,11 @@ def test_effects_returning_effects(self): - the result of that is returned. """ calls = [] - perform(func_dispatcher, - Effect(lambda box: box.succeed( - Effect(lambda box: calls.append("foo"))))) - self.assertEqual(calls, ['foo']) + perform( + func_dispatcher, + Effect(lambda box: box.succeed(Effect(lambda box: calls.append("foo")))), + ) + self.assertEqual(calls, ["foo"]) def test_effects_returning_effects_returning_effects(self): """ @@ -194,11 +193,17 @@ def test_effects_returning_effects_returning_effects(self): returned from the outermost effect's perform. """ calls = [] - perform(func_dispatcher, - Effect(lambda box: box.succeed( - Effect(lambda box: box.succeed( - Effect(lambda box: calls.append("foo"))))))) - self.assertEqual(calls, ['foo']) + perform( + func_dispatcher, + Effect( + lambda box: box.succeed( + Effect( + lambda box: box.succeed(Effect(lambda box: calls.append("foo"))) + ) + ) + ), + ) + self.assertEqual(calls, ["foo"]) def test_nested_effect_exception_passes_to_outer_error_handler(self): """ @@ -206,14 +211,14 @@ def test_nested_effect_exception_passes_to_outer_error_handler(self): exception is passed to the outer effect's error handlers. """ calls = [] - intent = lambda box: box.fail(ValueError('oh dear')) - perform(func_dispatcher, - Effect(lambda box: box.succeed(Effect(intent))) - .on(error=calls.append)) + intent = lambda box: box.fail(ValueError("oh dear")) + perform( + func_dispatcher, + Effect(lambda box: box.succeed(Effect(intent))).on(error=calls.append), + ) self.assertThat( - calls, - MatchesListwise([ - MatchesException(ValueError('oh dear'))])) + calls, MatchesListwise([MatchesException(ValueError("oh dear"))]) + ) def test_bounced(self): """ @@ -224,6 +229,7 @@ def test_bounced(self): def out_of_order(box): box.succeed("foo") calls.append("bar") + perform(func_dispatcher, Effect(out_of_order).on(success=calls.append)) self.assertEqual(calls, ["bar", "foo"]) @@ -235,9 +241,13 @@ def test_callbacks_bounced(self): def get_stack(_): calls.append(traceback.extract_stack()) - perform(func_dispatcher, - Effect(lambda box: box.succeed(None)) - .on(success=get_stack).on(success=get_stack)) + + perform( + func_dispatcher, + Effect(lambda box: box.succeed(None)) + .on(success=get_stack) + .on(success=get_stack), + ) self.assertEqual(calls[0], calls[1]) def test_effect_bounced(self): @@ -251,8 +261,9 @@ def get_stack(box): calls.append(traceback.extract_stack()) box.succeed(None) - perform(func_dispatcher, - Effect(get_stack).on(success=lambda _: Effect(get_stack))) + perform( + func_dispatcher, Effect(get_stack).on(success=lambda _: Effect(get_stack)) + ) self.assertEqual(calls[0], calls[1]) def test_asynchronous_callback_invocation(self): @@ -264,8 +275,8 @@ def test_asynchronous_callback_invocation(self): boxes = [] eff = Effect(boxes.append).on(success=results.append) perform(func_dispatcher, eff) - boxes[0].succeed('foo') - self.assertEqual(results, ['foo']) + boxes[0].succeed("foo") + self.assertEqual(results, ["foo"]) def test_asynchronous_callback_bounced(self): """ @@ -280,7 +291,7 @@ def get_stack(_): boxes = [] eff = Effect(boxes.append).on(success=get_stack).on(success=get_stack) perform(func_dispatcher, eff) - boxes[0].succeed('foo') + boxes[0].succeed("foo") self.assertEqual(calls[0], calls[1]) @@ -293,11 +304,11 @@ def test_caught(self): callable is invoked and its result is returned. """ try: - raise RuntimeError('foo') + raise RuntimeError("foo") except Exception as e: error = e - result = catch(RuntimeError, lambda e: ('caught', e))(error) - self.assertEqual(result, ('caught', error)) + result = catch(RuntimeError, lambda e: ("caught", e))(error) + self.assertEqual(result, ("caught", error)) def test_missed(self): """ @@ -305,10 +316,11 @@ def test_missed(self): the callable is not invoked and the original exception is reraised. """ try: - raise ZeroDivisionError('foo') + raise ZeroDivisionError("foo") except Exception as e: error = e e = self.assertRaises( ZeroDivisionError, - lambda: catch(RuntimeError, lambda e: ('caught', e))(error)) - self.assertEqual(str(e), 'foo') + lambda: catch(RuntimeError, lambda e: ("caught", e))(error), + ) + self.assertEqual(str(e), "foo") diff --git a/effect/test_do.py b/effect/test_do.py index 9470435..8f308f0 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -4,8 +4,15 @@ from py.test import raises from . import ( - ComposedDispatcher, Constant, Effect, Error, TypeDispatcher, - base_dispatcher, sync_perform, sync_performer) + ComposedDispatcher, + Constant, + Effect, + Error, + TypeDispatcher, + base_dispatcher, + sync_perform, + sync_performer, +) from .do import do, do_return @@ -18,8 +25,9 @@ def test_do_non_gf(): f = lambda: None with raises(TypeError) as err_info: perf(do(f)()) - assert (str(err_info.value) - == "%r is not a generator function. It returned None." % (f,)) + assert str( + err_info.value + ) == "%r is not a generator function. It returned None." % (f,) def test_do_return(): @@ -27,9 +35,11 @@ def test_do_return(): When a @do function yields a do_return, the given value becomes the eventual result. """ + @do def f(): yield do_return("hello") + assert perf(f()) == "hello" @@ -37,37 +47,45 @@ def test_do_return_effect(): @do def f(): yield do_return(Effect(Constant("hello"))) + assert perf(f()) == "hello" def test_yield_effect(): """Yielding an effect in @do results in the Effect's result.""" + @do def f(): x = yield Effect(Constant(3)) yield do_return(x) + perf(f()) == 3 def test_fall_off_the_end(): """Falling off the end results in None.""" + @do def f(): yield Effect(Constant(3)) + assert perf(f()) is None def test_yield_non_effect(): """Yielding a non-Effect results in a TypeError.""" + @do def f(): yield 1 + result = f() with raises(TypeError) as err_info: perf(result) assert str(err_info.value).startswith( - '@do functions must only yield Effects or results of ' - 'do_return. Got 1 from (3, 7): @@ -170,5 +197,6 @@ def f(): def test_py3_return(): """The `return x` syntax in Py3 sets the result of the Effect to `x`.""" from effect._test_do_py3 import py3_generator_with_return + eff = py3_generator_with_return() assert perf(eff) == 2 diff --git a/effect/test_fold.py b/effect/test_fold.py index 72a5b15..a7e1d9c 100644 --- a/effect/test_fold.py +++ b/effect/test_fold.py @@ -1,4 +1,3 @@ - import operator from pytest import raises @@ -15,16 +14,16 @@ def test_fold_effect(): :func:`fold_effect` folds the given function over the results of the effects. """ - effs = [Effect('a'), Effect('b'), Effect('c')] + effs = [Effect("a"), Effect("b"), Effect("c")] dispatcher = [ - ('a', lambda i: 'Ei'), - ('b', lambda i: 'Bee'), - ('c', lambda i: 'Cee'), + ("a", lambda i: "Ei"), + ("b", lambda i: "Bee"), + ("c", lambda i: "Cee"), ] - eff = fold_effect(operator.add, 'Nil', effs) + eff = fold_effect(operator.add, "Nil", effs) result = perform_sequence(dispatcher, eff) - assert result == 'NilEiBeeCee' + assert result == "NilEiBeeCee" def test_fold_effect_empty(): @@ -41,43 +40,46 @@ def test_fold_effect_errors(): When one of the effects in the folding list fails, a FoldError is raised with the accumulator so far. """ - effs = [Effect('a'), Effect(Error(ZeroDivisionError('foo'))), Effect('c')] + effs = [Effect("a"), Effect(Error(ZeroDivisionError("foo"))), Effect("c")] - dispatcher = [('a', lambda i: 'Ei')] + dispatcher = [("a", lambda i: "Ei")] - eff = fold_effect(operator.add, 'Nil', effs) + eff = fold_effect(operator.add, "Nil", effs) with raises(FoldError) as excinfo: perform_sequence(dispatcher, eff) - assert excinfo.value.accumulator == 'NilEi' - assert_that(excinfo.value.wrapped_exception, MatchesException(ZeroDivisionError('foo'))) + assert excinfo.value.accumulator == "NilEi" + assert_that( + excinfo.value.wrapped_exception, MatchesException(ZeroDivisionError("foo")) + ) def test_fold_effect_str(): """str()ing a FoldError returns useful traceback/exception info.""" - effs = [Effect('a'), Effect(Error(ZeroDivisionError('foo'))), Effect('c')] - dispatcher = [('a', lambda i: 'Ei')] + effs = [Effect("a"), Effect(Error(ZeroDivisionError("foo"))), Effect("c")] + dispatcher = [("a", lambda i: "Ei")] - eff = fold_effect(operator.add, 'Nil', effs) + eff = fold_effect(operator.add, "Nil", effs) with raises(FoldError) as excinfo: perform_sequence(dispatcher, eff) assert str(excinfo.value).startswith( - " Original traceback follows:\n") - assert str(excinfo.value).endswith('ZeroDivisionError: foo') + " Original traceback follows:\n" + ) + assert str(excinfo.value).endswith("ZeroDivisionError: foo") def test_sequence(): """Collects each Effectful result into a list.""" - effs = [Effect('a'), Effect('b'), Effect('c')] + effs = [Effect("a"), Effect("b"), Effect("c")] dispatcher = [ - ('a', lambda i: 'Ei'), - ('b', lambda i: 'Bee'), - ('c', lambda i: 'Cee'), + ("a", lambda i: "Ei"), + ("b", lambda i: "Bee"), + ("c", lambda i: "Cee"), ] eff = sequence(effs) result = perform_sequence(dispatcher, eff) - assert result == ['Ei', 'Bee', 'Cee'] + assert result == ["Ei", "Bee", "Cee"] def test_sequence_empty(): @@ -90,13 +92,15 @@ def test_sequence_error(): Allows :obj:`FoldError` to be raised when an Effect fails. The list accumulated so far is the `accumulator` value in the :obj:`FoldError`. """ - effs = [Effect('a'), Effect(Error(ZeroDivisionError('foo'))), Effect('c')] + effs = [Effect("a"), Effect(Error(ZeroDivisionError("foo"))), Effect("c")] - dispatcher = [('a', lambda i: 'Ei')] + dispatcher = [("a", lambda i: "Ei")] eff = sequence(effs) with raises(FoldError) as excinfo: perform_sequence(dispatcher, eff) - assert excinfo.value.accumulator == ['Ei'] - assert_that(excinfo.value.wrapped_exception, MatchesException(ZeroDivisionError('foo'))) + assert excinfo.value.accumulator == ["Ei"] + assert_that( + excinfo.value.wrapped_exception, MatchesException(ZeroDivisionError("foo")) + ) diff --git a/effect/test_intents.py b/effect/test_intents.py index 5216ffd..2cf3570 100644 --- a/effect/test_intents.py +++ b/effect/test_intents.py @@ -9,12 +9,18 @@ from ._dispatcher import ComposedDispatcher, TypeDispatcher from ._intents import ( base_dispatcher, - Constant, perform_constant, - Delay, perform_delay_with_sleep, - Error, perform_error, - Func, perform_func, + Constant, + perform_constant, + Delay, + perform_delay_with_sleep, + Error, + perform_error, + Func, + perform_func, FirstError, - ParallelEffects, parallel_all_errors) + ParallelEffects, + parallel_all_errors, +) from ._sync import sync_perform from ._test_utils import MatchesReraisedExcInfo from .parallel_async import perform_parallel_async @@ -24,9 +30,7 @@ def test_perform_constant(): """perform_constant returns the result of a Constant.""" intent = Constant("foo") - result = sync_perform( - TypeDispatcher({Constant: perform_constant}), - Effect(intent)) + result = sync_perform(TypeDispatcher({Constant: perform_constant}), Effect(intent)) assert result == "foo" @@ -40,29 +44,28 @@ def test_perform_error(): def test_perform_func(): """perform_func calls the function given in a Func.""" intent = Func(lambda: "foo") - result = sync_perform( - TypeDispatcher({Func: perform_func}), - Effect(intent)) + result = sync_perform(TypeDispatcher({Func: perform_func}), Effect(intent)) assert result == "foo" + def test_perform_func_args_kwargs(): """arbitrary positional and keyword arguments can be passed to Func.""" f = lambda *a, **kw: (a, kw) intent = Func(f, 1, 2, key=3) result = sync_perform(TypeDispatcher({Func: perform_func}), Effect(intent)) - assert result == ((1, 2), {'key': 3}) + assert result == ((1, 2), {"key": 3}) def test_first_error_str(): """FirstErrors have a pleasing format.""" - fe = FirstError(ValueError('foo'), index=150) - assert str(fe) == '(index=150) ValueError: foo' + fe = FirstError(ValueError("foo"), index=150) + assert str(fe) == "(index=150) ValueError: foo" def test_perform_delay_with_sleep(monkeypatch): """:func:`perform_delay_with_sleep` calls time.sleep.""" calls = [] - monkeypatch.setattr('time.sleep', calls.append) + monkeypatch.setattr("time.sleep", calls.append) disp = TypeDispatcher({Delay: perform_delay_with_sleep}) sync_perform(disp, Effect(Delay(3.7))) assert calls == [3.7] @@ -76,26 +79,26 @@ def test_parallel_all_errors(self): Exceptions raised from child effects get turned into (True, exc) results. """ - exc1 = EquitableException(message='foo') + exc1 = EquitableException(message="foo") reraise1 = partial(raise_, exc1) - exc2 = EquitableException(message='bar') + exc2 = EquitableException(message="bar") reraise2 = partial(raise_, exc2) - dispatcher = ComposedDispatcher([ - TypeDispatcher({ - ParallelEffects: perform_parallel_async, - }), - base_dispatcher]) - es = [Effect(Func(reraise1)), - Effect(Constant(1)), - Effect(Func(reraise2))] + dispatcher = ComposedDispatcher( + [ + TypeDispatcher({ParallelEffects: perform_parallel_async,}), + base_dispatcher, + ] + ) + es = [Effect(Func(reraise1)), Effect(Constant(1)), Effect(Func(reraise2))] eff = parallel_all_errors(es) self.assertThat( sync_perform(dispatcher, eff), - MatchesListwise([ - MatchesListwise([Equals(True), - MatchesReraisedExcInfo(exc1)]), - Equals((False, 1)), - MatchesListwise([Equals(True), - MatchesReraisedExcInfo(exc2)]), - ])) + MatchesListwise( + [ + MatchesListwise([Equals(True), MatchesReraisedExcInfo(exc1)]), + Equals((False, 1)), + MatchesListwise([Equals(True), MatchesReraisedExcInfo(exc2)]), + ] + ), + ) diff --git a/effect/test_io.py b/effect/test_io.py index 97ae861..8652f7c 100644 --- a/effect/test_io.py +++ b/effect/test_io.py @@ -6,13 +6,11 @@ def test_perform_display_print(capsys): """The stdio dispatcher has a performer that prints output.""" assert sync_perform(stdio_dispatcher, Effect(Display("foo"))) is None out, err = capsys.readouterr() - assert err == '' - assert out == 'foo\n' + assert err == "" + assert out == "foo\n" def test_perform_get_input_raw_input(monkeypatch): """The stdio dispatcher has a performer that reads input.""" - monkeypatch.setattr( - 'builtins.input', - lambda p: 'my name' if p == '> ' else 'boo') - assert sync_perform(stdio_dispatcher, Effect(Prompt('> '))) == 'my name' + monkeypatch.setattr("builtins.input", lambda p: "my name" if p == "> " else "boo") + assert sync_perform(stdio_dispatcher, Effect(Prompt("> "))) == "my name" diff --git a/effect/test_parallel_performers.py b/effect/test_parallel_performers.py index 8f90a71..e40129d 100644 --- a/effect/test_parallel_performers.py +++ b/effect/test_parallel_performers.py @@ -23,9 +23,7 @@ def test_empty(self): When given an empty list of effects, ``perform_parallel_async`` returns an empty list synchronusly. """ - result = sync_perform( - self.dispatcher, - parallel([])) + result = sync_perform(self.dispatcher, parallel([])) self.assertEqual(result, []) def test_parallel(self): @@ -34,28 +32,26 @@ def test_parallel(self): same order that they were passed to parallel. """ result = sync_perform( - self.dispatcher, - parallel([Effect(Constant('a')), - Effect(Constant('b'))])) - self.assertEqual(result, ['a', 'b']) + self.dispatcher, parallel([Effect(Constant("a")), Effect(Constant("b"))]) + ) + self.assertEqual(result, ["a", "b"]) def test_error(self): """ When given an effect that results in a Error, ``perform_parallel_async`` result in ``FirstError``. """ - expected_exc = EquitableException(message='foo') + expected_exc = EquitableException(message="foo") reraise = partial(raise_, expected_exc) try: - sync_perform( - self.dispatcher, - parallel([Effect(Func(reraise))])) + sync_perform(self.dispatcher, parallel([Effect(Func(reraise))])) except FirstError as fe: self.assertThat( fe, MatchesStructure( - index=Equals(0), - exception=MatchesReraisedExcInfo(expected_exc))) + index=Equals(0), exception=MatchesReraisedExcInfo(expected_exc) + ), + ) else: self.fail("sync_perform should have raised FirstError.") @@ -64,18 +60,19 @@ def test_error_index(self): The ``index`` of a :obj:`FirstError` is the index of the effect that failed in the list. """ - expected_exc = EquitableException(message='foo') + expected_exc = EquitableException(message="foo") reraise = partial(raise_, expected_exc) try: sync_perform( self.dispatcher, - parallel([ - Effect(Constant(1)), - Effect(Func(reraise)), - Effect(Constant(2))])) + parallel( + [Effect(Constant(1)), Effect(Func(reraise)), Effect(Constant(2))] + ), + ) except FirstError as fe: self.assertThat( fe, MatchesStructure( - index=Equals(1), - exception=MatchesReraisedExcInfo(expected_exc))) + index=Equals(1), exception=MatchesReraisedExcInfo(expected_exc) + ), + ) diff --git a/effect/test_ref.py b/effect/test_ref.py index f105ebe..af581e7 100644 --- a/effect/test_ref.py +++ b/effect/test_ref.py @@ -2,9 +2,7 @@ from ._base import Effect from ._sync import sync_perform -from .ref import ( - Reference, ModifyReference, ReadReference, - reference_dispatcher) +from .ref import Reference, ModifyReference, ReadReference, reference_dispatcher class ReferenceTests(TestCase): @@ -12,7 +10,7 @@ class ReferenceTests(TestCase): def test_read(self): """``read`` returns an Effect that represents the current value.""" - ref = Reference('initial') + ref = Reference("initial") self.assertEqual(ref.read(), Effect(ReadReference(ref=ref))) def test_modify(self): @@ -20,15 +18,13 @@ def test_modify(self): ref = Reference(0) transformer = lambda x: x + 1 eff = ref.modify(transformer) - self.assertEqual(eff, - Effect(ModifyReference(ref=ref, - transformer=transformer))) + self.assertEqual(eff, Effect(ModifyReference(ref=ref, transformer=transformer))) def test_perform_read(self): """Performing the reading results in the current value.""" - ref = Reference('initial') + ref = Reference("initial") result = sync_perform(reference_dispatcher, ref.read()) - self.assertEqual(result, 'initial') + self.assertEqual(result, "initial") def test_perform_modify(self): """ diff --git a/effect/test_retry.py b/effect/test_retry.py index e9271be..acfd221 100644 --- a/effect/test_retry.py +++ b/effect/test_retry.py @@ -7,13 +7,13 @@ class RetryTests(TestCase): - def test_should_not_retry(self): """retry raises the last error if should_retry returns False.""" - result = retry(ESError(RuntimeError("oh no!")), - lambda e: ESConstant(False)) - self.assertThat(lambda: resolve_stubs(base_dispatcher, result), - raises(RuntimeError("oh no!"))) + result = retry(ESError(RuntimeError("oh no!")), lambda e: ESConstant(False)) + self.assertThat( + lambda: resolve_stubs(base_dispatcher, result), + raises(RuntimeError("oh no!")), + ) def _repeated_effect_func(self, *funcs): """ @@ -35,10 +35,9 @@ def test_retry(self): again. """ func = self._repeated_effect_func( - lambda: raise_(RuntimeError("foo")), - lambda: "final") - result = retry(ESFunc(func), - lambda e: ESConstant(True)) + lambda: raise_(RuntimeError("foo")), lambda: "final" + ) + result = retry(ESFunc(func), lambda e: ESConstant(True)) self.assertEqual(resolve_stubs(base_dispatcher, result), "final") def test_continue_retrying(self): @@ -50,14 +49,16 @@ def test_continue_retrying(self): func = self._repeated_effect_func( lambda: raise_(RuntimeError("1")), lambda: raise_(RuntimeError("2")), - lambda: raise_(RuntimeError("3"))) + lambda: raise_(RuntimeError("3")), + ) def should_retry(e): return ESConstant(str(e) != "3") result = retry(ESFunc(func), should_retry) - self.assertThat(lambda: resolve_stubs(base_dispatcher, result), - raises(RuntimeError("3"))) + self.assertThat( + lambda: resolve_stubs(base_dispatcher, result), raises(RuntimeError("3")) + ) def raise_(exc): diff --git a/effect/test_sync.py b/effect/test_sync.py index 79913ed..e473472 100644 --- a/effect/test_sync.py +++ b/effect/test_sync.py @@ -11,6 +11,7 @@ def func_dispatcher(intent): def performer(dispatcher, intent, box): intent(box) + return performer @@ -22,27 +23,28 @@ def test_sync_perform_effect_function_dispatch(self): sync_perform returns the result of the effect. """ intent = lambda box: box.succeed("foo") - self.assertEqual( - sync_perform(func_dispatcher, Effect(intent)), - 'foo') + self.assertEqual(sync_perform(func_dispatcher, Effect(intent)), "foo") def test_sync_perform_async_effect(self): """If an effect is asynchronous, sync_effect raises an error.""" intent = lambda box: None self.assertRaises( - NotSynchronousError, - lambda: sync_perform(func_dispatcher, Effect(intent))) + NotSynchronousError, lambda: sync_perform(func_dispatcher, Effect(intent)) + ) def test_error_bubbles_up(self): """ When effect performance fails, the exception is raised up through sync_perform. """ + def fail(box): box.fail(ValueError("oh dear")) + self.assertThat( lambda: sync_perform(func_dispatcher, Effect(fail)), - raises(ValueError('oh dear'))) + raises(ValueError("oh dear")), + ) class SyncPerformerTests(TestCase): @@ -52,6 +54,7 @@ class SyncPerformerTests(TestCase): def test_success(self): """Return value of the performer becomes the result of the Effect.""" + @sync_performer def succeed(dispatcher, intent): return intent @@ -64,19 +67,22 @@ def test_failure(self): """ Errors are caught and cause the effect to fail with the exception info. """ + @sync_performer def fail(dispatcher, intent): raise intent dispatcher = lambda _: fail self.assertThat( - sync_perform(dispatcher, - Effect(ValueError('oh dear')).on(error=lambda e: e)), - MatchesException(ValueError('oh dear'))) + sync_perform( + dispatcher, Effect(ValueError("oh dear")).on(error=lambda e: e) + ), + MatchesException(ValueError("oh dear")), + ) def test_instance_method_performer(self): """The decorator works on instance methods.""" - eff = Effect('meaningless') + eff = Effect("meaningless") class PerformerContainer(object): @sync_performer @@ -87,41 +93,46 @@ def performer(self, dispatcher, intent): dispatcher = lambda i: container.performer result = sync_perform(dispatcher, eff) - self.assertEqual(result, (container, dispatcher, 'meaningless')) + self.assertEqual(result, (container, dispatcher, "meaningless")) def test_promote_metadata(self): """ The decorator copies metadata from the wrapped function onto the wrapper. """ + def original(dispatcher, intent): """Original!""" pass + original.attr = 1 wrapped = sync_performer(original) - self.assertEqual(wrapped.__name__, 'original') + self.assertEqual(wrapped.__name__, "original") self.assertEqual(wrapped.attr, 1) - self.assertEqual(wrapped.__doc__, 'Original!') + self.assertEqual(wrapped.__doc__, "Original!") def test_ignore_lack_of_metadata(self): """ When the original callable is not a function, a new function is still returned. """ + def original(something, dispatcher, intent): """Original!""" pass - new_func = partial(original, 'something') + + new_func = partial(original, "something") original.attr = 1 wrapped = sync_performer(new_func) - self.assertEqual(wrapped.__name__, 'sync_wrapper') + self.assertEqual(wrapped.__name__, "sync_wrapper") def test_kwargs(self): """Additional kwargs are passed through.""" + @sync_performer def p(dispatcher, intent, extra): return extra - dispatcher = lambda _: partial(p, extra='extra val') - result = sync_perform(dispatcher, Effect('foo')) - self.assertEqual(result, 'extra val') + dispatcher = lambda _: partial(p, extra="extra val") + result = sync_perform(dispatcher, Effect("foo")) + self.assertEqual(result, "extra val") diff --git a/effect/test_testing.py b/effect/test_testing.py index 06cf23d..5f81be8 100644 --- a/effect/test_testing.py +++ b/effect/test_testing.py @@ -16,7 +16,8 @@ base_dispatcher, parallel, sync_perform, - sync_performer) + sync_performer, +) from .do import do, do_return from .fold import FoldError, sequence from .testing import ( @@ -35,12 +36,12 @@ parallel_sequence, perform_sequence, resolve_effect, - resolve_stubs) + resolve_stubs, +) from ._test_utils import MatchesException class ResolveEffectTests(TestCase): - def test_basic_resolution(self): """ When no callbacks are attached to the effect, the result argument is @@ -56,6 +57,7 @@ def test_invoke_callbacks(self): def add1(n): return n + 1 + eff = Effect(None).on(success=add1).on(success=add1) self.assertEqual(resolve_effect(eff, 0), 2) @@ -63,12 +65,11 @@ def test_callback_returning_effect(self): """ When a callback returns an effect, that effect is returned. """ - stub_effect = Effect('inner') + stub_effect = Effect("inner") eff = Effect(None).on(success=lambda r: stub_effect) - result = resolve_effect(eff, 'foo') - self.assertEqual(result.intent, 'inner') - self.assertEqual(resolve_effect(result, 'next-result'), - 'next-result') + result = resolve_effect(eff, "foo") + self.assertEqual(result.intent, "inner") + self.assertEqual(resolve_effect(result, "next-result"), "next-result") def test_intermediate_callback_returning_effect(self): """ @@ -83,11 +84,12 @@ def a(r): def b(r): return ("b-result", r) + eff = Effect("orig").on(success=a).on(success=b) result = resolve_effect(eff, "foo") self.assertEqual( - resolve_effect(result, "next-result"), - ('b-result', 'next-result')) + resolve_effect(result, "next-result"), ("b-result", "next-result") + ) def test_maintain_intermediate_effect_callbacks(self): """ @@ -105,10 +107,13 @@ def nested_b(r): def c(r): return ("c-result", r) + eff = Effect("orig").on(success=a).on(success=c) result = resolve_effect(eff, "foo") - self.assertEqual(resolve_effect(result, 'next-result'), - ('c-result', ('nested-b-result', 'next-result'))) + self.assertEqual( + resolve_effect(result, "next-result"), + ("c-result", ("nested-b-result", "next-result")), + ) def test_resolve_effect_cb_exception(self): """ @@ -118,12 +123,17 @@ def test_resolve_effect_cb_exception(self): self.assertThat( resolve_effect( Effect("orig") - .on(success=lambda r: 1 / 0) - .on(error=lambda exc: ('handled', exc)), - 'result'), - MatchesListwise([ - Equals('handled'), - MatchesException(ZeroDivisionError('division by zero'))])) + .on(success=lambda r: 1 / 0) + .on(error=lambda exc: ("handled", exc)), + "result", + ), + MatchesListwise( + [ + Equals("handled"), + MatchesException(ZeroDivisionError("division by zero")), + ] + ), + ) def test_raise_if_final_result_is_error(self): """ @@ -131,12 +141,12 @@ def test_raise_if_final_result_is_error(self): resolve_effect. """ self.assertThat( - lambda: - resolve_effect( - Effect('orig').on( - success=lambda r: _raise(ValueError('oh goodness'))), - 'result'), - raises(ValueError('oh goodness'))) + lambda: resolve_effect( + Effect("orig").on(success=lambda r: _raise(ValueError("oh goodness"))), + "result", + ), + raises(ValueError("oh goodness")), + ) def test_fail_effect(self): """ @@ -144,20 +154,16 @@ def test_fail_effect(self): handler is invoked. """ self.assertThat( - lambda: - fail_effect( - Effect('orig'), - ValueError('oh deary me')), - raises(ValueError('oh deary me'))) + lambda: fail_effect(Effect("orig"), ValueError("oh deary me")), + raises(ValueError("oh deary me")), + ) def test_skip_callbacks(self): """ Intermediate callbacks of the wrong type are skipped. """ - eff = (Effect('foo') - .on(error=lambda f: 1) - .on(success=lambda x: ('succeeded', x))) - self.assertEqual(resolve_effect(eff, 'foo'), ('succeeded', 'foo')) + eff = Effect("foo").on(error=lambda f: 1).on(success=lambda x: ("succeeded", x)) + self.assertEqual(resolve_effect(eff, "foo"), ("succeeded", "foo")) class ResolveStubsTests(TestCase): @@ -170,7 +176,9 @@ def test_resolve_stubs(self): """ eff = ESConstant("foo").on( success=lambda r: ESError(RuntimeError("foo")).on( - error=lambda e: ESFunc(lambda: "heyo"))) + error=lambda e: ESFunc(lambda: "heyo") + ) + ) self.assertEqual(resolve_stubs(base_dispatcher, eff), "heyo") def test_non_test_intent(self): @@ -201,8 +209,7 @@ def test_resolve_stubs_callbacks_only_invoked_once(self): This is a regression test for a really dumb bug. """ eff = ESConstant("foo").on(success=lambda r: ("got it", r)) - self.assertEqual(resolve_stubs(base_dispatcher, eff), - ("got it", "foo")) + self.assertEqual(resolve_stubs(base_dispatcher, eff), ("got it", "foo")) def test_outer_callbacks_after_intermediate_effect(self): """ @@ -210,10 +217,11 @@ def test_outer_callbacks_after_intermediate_effect(self): callbacks, the remaining callbacks will be wrapped around the returned effect. """ - eff = ESConstant("foo").on( - success=lambda r: Effect("something") - ).on( - lambda r: ("callbacked", r)) + eff = ( + ESConstant("foo") + .on(success=lambda r: Effect("something")) + .on(lambda r: ("callbacked", r)) + ) result = resolve_stubs(base_dispatcher, eff) self.assertIs(type(result), Effect) self.assertEqual(result.intent, "something") @@ -230,9 +238,7 @@ def test_parallel_non_stubs(self): If a parallel effect contains a non-stub, the parallel effect is returned as-is. """ - p_eff = parallel( - [ESConstant(1), Effect(Constant(2))] - ).on(lambda x: 0) + p_eff = parallel([ESConstant(1), Effect(Constant(2))]).on(lambda x: 0) self.assertEqual(resolve_stubs(base_dispatcher, p_eff), p_eff) def test_parallel_stubs_with_callbacks(self): @@ -249,7 +255,8 @@ def test_parallel_stubs_with_callbacks_returning_effects(self): callbacks of parallel effects. """ p_eff = parallel([ESConstant(1), ESConstant(2)]).on( - lambda r: ESConstant(r[0] + 1)) + lambda r: ESConstant(r[0] + 1) + ) self.assertEqual(resolve_stubs(base_dispatcher, p_eff), 2) def test_parallel_stubs_with_element_callbacks_returning_non_stubs(self): @@ -258,8 +265,7 @@ def test_parallel_stubs_with_element_callbacks_returning_non_stubs(self): NOT be performed. """ p_eff = parallel([ESConstant(1).on(lambda r: Effect(Constant(2)))]) - self.assertEqual(resolve_stubs(base_dispatcher, p_eff), - [Effect(Constant(2))]) + self.assertEqual(resolve_stubs(base_dispatcher, p_eff), [Effect(Constant(2))]) def _raise(e): @@ -272,12 +278,12 @@ class EQDispatcherTests(TestCase): def test_no_intent(self): """When the dispatcher can't match the intent, it returns None.""" d = EQDispatcher([]) - self.assertIs(d('foo'), None) + self.assertIs(d("foo"), None) def test_perform(self): """When an intent matches, performing it returns the canned result.""" - d = EQDispatcher([('hello', 'there')]) - self.assertEqual(sync_perform(d, Effect('hello')), 'there') + d = EQDispatcher([("hello", "there")]) + self.assertEqual(sync_perform(d, Effect("hello")), "there") class EQFDispatcherTests(TestCase): @@ -286,12 +292,12 @@ class EQFDispatcherTests(TestCase): def test_no_intent(self): """When the dispatcher can't match the intent, it returns None.""" d = EQFDispatcher([]) - self.assertIs(d('foo'), None) + self.assertIs(d("foo"), None) def test_perform(self): """When an intent matches, performing it returns the canned result.""" - d = EQFDispatcher([('hello', lambda i: (i, 'there'))]) - self.assertEqual(sync_perform(d, Effect('hello')), ('hello', 'there')) + d = EQFDispatcher([("hello", lambda i: (i, "there"))]) + self.assertEqual(sync_perform(d, Effect("hello")), ("hello", "there")) class SequenceDispatcherTests(TestCase): @@ -301,35 +307,39 @@ def test_mismatch(self): """ When an intent isn't expected, a None is returned. """ - d = SequenceDispatcher([('foo', lambda i: 1 / 0)]) - self.assertEqual(d('hello'), None) + d = SequenceDispatcher([("foo", lambda i: 1 / 0)]) + self.assertEqual(d("hello"), None) def test_success(self): """ Each intent is performed in sequence with the provided functions, as long as the intents match. """ - d = SequenceDispatcher([ - ('foo', lambda i: ('performfoo', i)), - ('bar', lambda i: ('performbar', i)), - ]) - eff = Effect('foo').on(lambda r: Effect('bar').on(lambda r2: (r, r2))) + d = SequenceDispatcher( + [ + ("foo", lambda i: ("performfoo", i)), + ("bar", lambda i: ("performbar", i)), + ] + ) + eff = Effect("foo").on(lambda r: Effect("bar").on(lambda r2: (r, r2))) self.assertEqual( - sync_perform(d, eff), - (('performfoo', 'foo'), ('performbar', 'bar'))) + sync_perform(d, eff), (("performfoo", "foo"), ("performbar", "bar")) + ) def test_ran_out(self): """When there are no more items left, None is returned.""" d = SequenceDispatcher([]) - self.assertEqual(d('foo'), None) + self.assertEqual(d("foo"), None) def test_out_of_order(self): """Order of items in the sequence matters.""" - d = SequenceDispatcher([ - ('bar', lambda i: ('performbar', i)), - ('foo', lambda i: ('performfoo', i)), - ]) - self.assertEqual(d('foo'), None) + d = SequenceDispatcher( + [ + ("bar", lambda i: ("performbar", i)), + ("foo", lambda i: ("performfoo", i)), + ] + ) + self.assertEqual(d("foo"), None) def test_consumed(self): """`consumed` returns True if there are no more elements.""" @@ -341,30 +351,31 @@ def test_consumed_honors_changes(self): `consumed` returns True if there are no more elements after performing some. """ - d = SequenceDispatcher([('foo', lambda i: 'bar')]) - sync_perform(d, Effect('foo')) + d = SequenceDispatcher([("foo", lambda i: "bar")]) + sync_perform(d, Effect("foo")) self.assertTrue(d.consumed()) def test_not_consumed(self): """ `consumed` returns False if there are more elements. """ - d = SequenceDispatcher([('foo', lambda i: 'bar')]) + d = SequenceDispatcher([("foo", lambda i: "bar")]) self.assertFalse(d.consumed()) def test_consume_good(self): """``consume`` doesn't raise an error if all elements are consumed.""" - d = SequenceDispatcher([('foo', lambda i: 'bar')]) + d = SequenceDispatcher([("foo", lambda i: "bar")]) with d.consume(): - sync_perform(d, Effect('foo')) + sync_perform(d, Effect("foo")) def test_consume_raises(self): """``consume`` raises an error if not all elements are consumed.""" - d = SequenceDispatcher([('foo', None)]) + d = SequenceDispatcher([("foo", None)]) def failer(): with d.consume(): pass + e = self.assertRaises(AssertionError, failer) self.assertEqual(str(e), "Not all intents were performed: ['foo']") @@ -384,14 +395,16 @@ def test_perform_sequence(): @do def code_under_test(): - r = yield Effect(MyIntent('a')) - r2 = yield Effect(OtherIntent('b')) + r = yield Effect(MyIntent("a")) + r2 = yield Effect(OtherIntent("b")) yield do_return((r, r2)) - seq = [(MyIntent('a'), lambda i: 'result1'), - (OtherIntent('b'), lambda i: 'result2')] + seq = [ + (MyIntent("a"), lambda i: "result1"), + (OtherIntent("b"), lambda i: "result2"), + ] eff = code_under_test() - assert perform_sequence(seq, eff) == ('result1', 'result2') + assert perform_sequence(seq, eff) == ("result1", "result2") def test_perform_sequence_log(): @@ -399,18 +412,18 @@ def test_perform_sequence_log(): When an intent isn't found, a useful log of intents is included in the exception message. """ + @do def code_under_test(): - r = yield Effect(MyIntent('a')) - r2 = yield Effect(OtherIntent('b')) + r = yield Effect(MyIntent("a")) + r2 = yield Effect(OtherIntent("b")) yield do_return((r, r2)) - seq = [(MyIntent('a'), lambda i: 'result1')] + seq = [(MyIntent("a"), lambda i: "result1")] with pytest.raises(AssertionError) as exc: perform_sequence(seq, code_under_test()) - expected = ("sequence: MyIntent(val='a')\n" - "NOT FOUND: OtherIntent(val='b')") + expected = "sequence: MyIntent(val='a')\n" "NOT FOUND: OtherIntent(val='b')" assert expected in str(exc.value) @@ -420,14 +433,16 @@ def test_parallel_sequence(): order, and returns the results associated with those intents. """ seq = [ - parallel_sequence([ - [(1, lambda i: "one!")], - [(2, lambda i: "two!")], - [(3, lambda i: "three!")], - ]) + parallel_sequence( + [ + [(1, lambda i: "one!")], + [(2, lambda i: "two!")], + [(3, lambda i: "three!")], + ] + ) ] p = parallel([Effect(1), Effect(2), Effect(3)]) - assert perform_sequence(seq, p) == ['one!', 'two!', 'three!'] + assert perform_sequence(seq, p) == ["one!", "two!", "three!"] def test_parallel_sequence_fallback(): @@ -435,20 +450,24 @@ def test_parallel_sequence_fallback(): Accepts a ``fallback`` dispatcher that will be used when the sequence doesn't contain an intent. """ + def dispatch_2(intent): if intent == 2: return sync_performer(lambda d, i: "two!") + fallback = ComposedDispatcher([dispatch_2, base_dispatcher]) seq = [ - parallel_sequence([ - [(1, lambda i: 'one!')], - [], # only implicit effects in this slot - [(3, lambda i: 'three!')], - ], - fallback_dispatcher=fallback), + parallel_sequence( + [ + [(1, lambda i: "one!")], + [], # only implicit effects in this slot + [(3, lambda i: "three!")], + ], + fallback_dispatcher=fallback, + ), ] p = parallel([Effect(1), Effect(2), Effect(3)]) - assert perform_sequence(seq, p) == ['one!', 'two!', 'three!'] + assert perform_sequence(seq, p) == ["one!", "two!", "three!"] def test_parallel_sequence_must_be_parallel(): @@ -457,11 +476,13 @@ def test_parallel_sequence_must_be_parallel(): match and a FoldError of NoPerformerFoundError will be raised. """ seq = [ - parallel_sequence([ - [(1, lambda i: "one!")], - [(2, lambda i: "two!")], - [(3, lambda i: "three!")], - ]) + parallel_sequence( + [ + [(1, lambda i: "one!")], + [(2, lambda i: "two!")], + [(3, lambda i: "three!")], + ] + ) ] p = sequence([Effect(1), Effect(2), Effect(3)]) with pytest.raises(FoldError) as excinfo: @@ -493,11 +514,14 @@ def code_under_test(): yield do_return((r, r2)) seq = [ - (WrappedIntent(_ANY, "field"), nested_sequence([(1, const("r1")), (2, const("r2"))])), - (MyIntent("a"), const("result2")) + ( + WrappedIntent(_ANY, "field"), + nested_sequence([(1, const("r1")), (2, const("r2"))]), + ), + (MyIntent("a"), const("result2")), ] eff = code_under_test() - assert perform_sequence(seq, eff) == ('wrap', 'result2') + assert perform_sequence(seq, eff) == ("wrap", "result2") def test_const(): diff --git a/effect/test_threads.py b/effect/test_threads.py index 4033e30..ad5c9b7 100644 --- a/effect/test_threads.py +++ b/effect/test_threads.py @@ -17,6 +17,6 @@ def setUp(self): super(ParallelPoolPerformerTests, self).setUp() self.pool = ThreadPool() self.p_performer = partial(perform_parallel_with_pool, self.pool) - self.dispatcher = ComposedDispatcher([ - base_dispatcher, - TypeDispatcher({ParallelEffects: self.p_performer})]) + self.dispatcher = ComposedDispatcher( + [base_dispatcher, TypeDispatcher({ParallelEffects: self.p_performer})] + ) diff --git a/effect/testing.py b/effect/testing.py index c978ca4..d673ddf 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -16,22 +16,24 @@ __all__ = [ - 'perform_sequence', - 'parallel_sequence', - 'nested_sequence', - 'SequenceDispatcher', - 'noop', - 'const', - 'conste', - 'intent_func', - 'resolve_effect', - 'fail_effect', - 'EQDispatcher', - 'EQFDispatcher', - 'Stub', - 'ESConstant', 'ESError', 'ESFunc', - 'resolve_stubs', - 'resolve_stub', + "perform_sequence", + "parallel_sequence", + "nested_sequence", + "SequenceDispatcher", + "noop", + "const", + "conste", + "intent_func", + "resolve_effect", + "fail_effect", + "EQDispatcher", + "EQFDispatcher", + "Stub", + "ESConstant", + "ESError", + "ESFunc", + "resolve_stubs", + "resolve_stub", ] @@ -87,13 +89,12 @@ def test_code(): used. :return: Result of performed sequence """ + def fmt_log(): - next_item = '' + next_item = "" if len(sequence.sequence) > 0: - next_item = '\nNEXT EXPECTED: %s' % (sequence.sequence[0][0],) - return '{{{\n%s%s\n}}}' % ( - '\n'.join('%s: %s' % x for x in log), - next_item) + next_item = "\nNEXT EXPECTED: %s" % (sequence.sequence[0][0],) + return "{{{\n%s%s\n}}}" % ("\n".join("%s: %s" % x for x in log), next_item) def dispatcher(intent): p = sequence(intent) @@ -107,8 +108,8 @@ def dispatcher(intent): else: log.append(("NOT FOUND", intent)) raise AssertionError( - "Performer not found: %s! Log follows:\n%s" % ( - intent, fmt_log())) + "Performer not found: %s! Log follows:\n%s" % (intent, fmt_log()) + ) if fallback_dispatcher is None: fallback_dispatcher = base_dispatcher @@ -120,8 +121,11 @@ def dispatcher(intent): @object.__new__ class _ANY(object): - def __eq__(self, o): return True - def __ne__(self, o): return False + def __eq__(self, o): + return True + + def __ne__(self, o): + return False def parallel_sequence(parallel_seqs, fallback_dispatcher=None): @@ -173,9 +177,16 @@ def performer(intent): "Need one list in parallel_seqs per parallel effect. " "Got %s effects and %s seqs.\n" "Effects: %s\n" - "parallel_seqs: %s" % (len(intent.effects), len(parallel_seqs), - intent.effects, parallel_seqs)) + "parallel_seqs: %s" + % ( + len(intent.effects), + len(parallel_seqs), + intent.effects, + parallel_seqs, + ) + ) return list(map(perf, parallel_seqs, intent.effects)) + return (ParallelEffects(effects=_ANY), performer) @@ -191,6 +202,7 @@ class Stub(object): :class:`Stub` is intentionally not performable by any default mechanism. """ + intent = attr.ib() @@ -256,8 +268,8 @@ def resolve_effect(effect, result, is_error=False): is_error, result = guard(cb, result) if type(result) is Effect: return Effect( - result.intent, - callbacks=result.callbacks + effect.callbacks[i + 1:]) + result.intent, callbacks=result.callbacks + effect.callbacks[i + 1 :] + ) if is_error: raise result return result @@ -292,18 +304,16 @@ def resolve_stub(dispatcher, effect): if len(result_slot) == 0: raise NotSynchronousError( "Performer %r was not synchronous during stub resolution for " - "effect %r" - % (performer, effect)) + "effect %r" % (performer, effect) + ) if len(result_slot) > 1: raise RuntimeError( "Pathological error (too many box results) while running " - "performer %r for effect %r" - % (performer, effect)) - return resolve_effect(effect, result_slot[0][1], - is_error=result_slot[0][0]) + "performer %r for effect %r" % (performer, effect) + ) + return resolve_effect(effect, result_slot[0][1], is_error=result_slot[0][0]) else: - raise TypeError("resolve_stub can only resolve stubs, not %r" - % (effect,)) + raise TypeError("resolve_stub can only resolve stubs, not %r" % (effect,)) def resolve_stubs(dispatcher, effect): @@ -323,14 +333,15 @@ def resolve_stubs(dispatcher, effect): if type(effect.intent) is Stub: effect = resolve_stub(dispatcher, effect) elif type(effect.intent) is ParallelEffects: - if not all(isinstance(x.intent, Stub) - for x in effect.intent.effects): + if not all(isinstance(x.intent, Stub) for x in effect.intent.effects): break else: effect = resolve_effect( effect, - list(map(partial(resolve_stubs, dispatcher), - effect.intent.effects))) + list( + map(partial(resolve_stubs, dispatcher), effect.intent.effects) + ), + ) else: break @@ -369,6 +380,7 @@ class EQDispatcher(object): :param list mapping: A sequence of tuples of (intent, result). """ + mapping = attr.ib() def __call__(self, intent): @@ -413,6 +425,7 @@ class EQFDispatcher(object): :param list mapping: A sequence of two-tuples of (intent, function). """ + mapping = attr.ib() def __call__(self, intent): @@ -442,6 +455,7 @@ class SequenceDispatcher(object): :param list sequence: Sequence of (intent, fn). """ + sequence = attr.ib() def __call__(self, intent): @@ -466,11 +480,14 @@ def consume(self): if not self.consumed(): raise AssertionError( "Not all intents were performed: {0}".format( - [x[0] for x in self.sequence])) + [x[0] for x in self.sequence] + ) + ) -def nested_sequence(seq, get_effect=attrgetter('effect'), - fallback_dispatcher=base_dispatcher): +def nested_sequence( + seq, get_effect=attrgetter("effect"), fallback_dispatcher=base_dispatcher +): """ Return a function of Intent -> a that performs an effect retrieved from the intent (by accessing its `effect` attribute, by default) with the given @@ -499,6 +516,7 @@ def nested_sequence(seq, get_effect=attrgetter('effect'), sequence dispatcher. :return: ``callable`` that can be used as performer of a wrapped intent """ + def performer(intent): effect = get_effect(intent) return perform_sequence(seq, effect, fallback_dispatcher=fallback_dispatcher) diff --git a/effect/threads.py b/effect/threads.py index 6696043..7e915c4 100644 --- a/effect/threads.py +++ b/effect/threads.py @@ -34,4 +34,5 @@ def perform_child(index_and_effect): return sync_perform(dispatcher, effect) except Exception as e: raise FirstError(exception=e, index=index) + return pool.map(perform_child, enumerate(parallel_effects.effects)) diff --git a/setup.py b/setup.py index a0b09ca..5439395 100644 --- a/setup.py +++ b/setup.py @@ -5,14 +5,14 @@ name="effect", version="0.12.0+", description="pure effects for Python", - long_description=open('README.rst').read(), + long_description=open("README.rst").read(), url="http://github.com/python-effect/effect/", author="Christopher Armstrong", license="MIT", classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3', + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", ], - packages=['effect'], - install_requires=['attrs'], + packages=["effect"], + install_requires=["attrs"], ) From a5c944fa39e5c1f1dfa47274c8f36fa9feaa2bcf Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:03:23 -0600 Subject: [PATCH 40/54] oh yeah, add black to the dev requirements --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 6bc8db6..6d1b5f3 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ +black testtools flake8 pytest From 856f5df50a8e5fa965596c4ff92ccc4a86fd34d2 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:07:15 -0600 Subject: [PATCH 41/54] fix lints --- effect/_intents.py | 2 +- effect/_test_utils.py | 2 +- effect/io.py | 2 +- effect/test_intents.py | 2 +- effect/testing.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/effect/_intents.py b/effect/_intents.py index 3b38efe..f8307e2 100644 --- a/effect/_intents.py +++ b/effect/_intents.py @@ -199,5 +199,5 @@ def perform_func(dispatcher, intent): base_dispatcher = TypeDispatcher( - {Constant: perform_constant, Error: perform_error, Func: perform_func,} + {Constant: perform_constant, Error: perform_error, Func: perform_func} ) diff --git a/effect/_test_utils.py b/effect/_test_utils.py index 46ea60f..caea89f 100644 --- a/effect/_test_utils.py +++ b/effect/_test_utils.py @@ -52,7 +52,7 @@ def match(self, actual): traceback.TracebackException.from_exception(self.expected).format() ) new = list(traceback.TracebackException.from_exception(actual).format()) - tail_equals = lambda a, b: a == b[-len(a) :] + tail_equals = lambda a, b: a == b[-len(a):] if not tail_equals(expected[1:], new[1:]): return ReraisedTracebackMismatch(expected_tb=expected, got_tb=new) diff --git a/effect/io.py b/effect/io.py index d6c127a..50277f6 100644 --- a/effect/io.py +++ b/effect/io.py @@ -38,5 +38,5 @@ def perform_get_input_raw_input(dispatcher, intent): stdio_dispatcher = TypeDispatcher( - {Display: perform_display_print, Prompt: perform_get_input_raw_input,} + {Display: perform_display_print, Prompt: perform_get_input_raw_input} ) diff --git a/effect/test_intents.py b/effect/test_intents.py index 2cf3570..85987b4 100644 --- a/effect/test_intents.py +++ b/effect/test_intents.py @@ -86,7 +86,7 @@ def test_parallel_all_errors(self): dispatcher = ComposedDispatcher( [ - TypeDispatcher({ParallelEffects: perform_parallel_async,}), + TypeDispatcher({ParallelEffects: perform_parallel_async}), base_dispatcher, ] ) diff --git a/effect/testing.py b/effect/testing.py index d673ddf..67e84e2 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -268,7 +268,7 @@ def resolve_effect(effect, result, is_error=False): is_error, result = guard(cb, result) if type(result) is Effect: return Effect( - result.intent, callbacks=result.callbacks + effect.callbacks[i + 1 :] + result.intent, callbacks=result.callbacks + effect.callbacks[i + 1:] ) if is_error: raise result From b7164941b9274636e34dae01c921c0e42e4e0cf5 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:09:19 -0600 Subject: [PATCH 42/54] ok I guess black and flake8 disagree, so we have to go with black here (since it's not configurable) --- effect/_test_utils.py | 2 +- effect/testing.py | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/effect/_test_utils.py b/effect/_test_utils.py index caea89f..46ea60f 100644 --- a/effect/_test_utils.py +++ b/effect/_test_utils.py @@ -52,7 +52,7 @@ def match(self, actual): traceback.TracebackException.from_exception(self.expected).format() ) new = list(traceback.TracebackException.from_exception(actual).format()) - tail_equals = lambda a, b: a == b[-len(a):] + tail_equals = lambda a, b: a == b[-len(a) :] if not tail_equals(expected[1:], new[1:]): return ReraisedTracebackMismatch(expected_tb=expected, got_tb=new) diff --git a/effect/testing.py b/effect/testing.py index 67e84e2..d673ddf 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -268,7 +268,7 @@ def resolve_effect(effect, result, is_error=False): is_error, result = guard(cb, result) if type(result) is Effect: return Effect( - result.intent, callbacks=result.callbacks + effect.callbacks[i + 1:] + result.intent, callbacks=result.callbacks + effect.callbacks[i + 1 :] ) if is_error: raise result diff --git a/setup.cfg b/setup.cfg index 846eb4b..2a28575 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,4 +6,4 @@ universal=1 [flake8] max-line-length = 100 -ignore = E131,E301,E302,E731,W503,E701,E704,E722 +ignore = E131,E301,E302,E731,W503,E701,E704,E722,E203 From b0a8ab50d7dc675b26efad6ad43023a7b6c5f587 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:14:55 -0600 Subject: [PATCH 43/54] black doesn't seem to be working on pypy... so I am committing an "env" command to the travis script to see what environment variables are available to me to check before running `black`, so I can only run black on non-pypy versions. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7ee2267..fceefe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ install: - pip install sphinx sphinx_rtd_theme script: - flake8 + - env - black --check . - pytest - make doc From dbbb86571aeba9fd42eba47d487dc4dbffba8106 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:21:35 -0600 Subject: [PATCH 44/54] ok let's just take black out of the requirements and install it conditionally in travis --- .travis.yml | 5 +++-- dev-requirements.txt | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fceefe1..242d3ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,10 +10,11 @@ install: - pip install . - pip install -r dev-requirements.txt - pip install sphinx sphinx_rtd_theme + # black isn't installing on pypy3, so just skip it + - [ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && pip install black script: - flake8 - - env - - black --check . + - [ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && black --check . - pytest - make doc diff --git a/dev-requirements.txt b/dev-requirements.txt index 6d1b5f3..6bc8db6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,3 @@ -black testtools flake8 pytest From 38627ab823069b527846699151490106118130ba Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:26:24 -0600 Subject: [PATCH 45/54] oops, yaml syntax --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 242d3ad..32fda21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,10 @@ install: - pip install -r dev-requirements.txt - pip install sphinx sphinx_rtd_theme # black isn't installing on pypy3, so just skip it - - [ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && pip install black + - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && pip install black' script: - flake8 - - [ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && black --check . + - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && black --check .' - pytest - make doc From 010252cb8288df4f1dac1c81a549c47dd0068fa0 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:28:03 -0600 Subject: [PATCH 46/54] oops bash syntax --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32fda21..85f393d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,10 @@ install: - pip install -r dev-requirements.txt - pip install sphinx sphinx_rtd_theme # black isn't installing on pypy3, so just skip it - - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && pip install black' + - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3" ] && pip install black' script: - flake8 - - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3"] && black --check .' + - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3" ] && black --check .' - pytest - make doc From 8f4565f2423a31e0e6290a94df0d78cc882c9e7f Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:32:13 -0600 Subject: [PATCH 47/54] use an if statement instead of a && because the && was resulting in a failing exit code --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 85f393d..7c1b3b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,10 @@ install: - pip install -r dev-requirements.txt - pip install sphinx sphinx_rtd_theme # black isn't installing on pypy3, so just skip it - - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3" ] && pip install black' + - 'if [ "$TRAVIS_PYTHON_VERSION" != "pypy3" ]; then pip install black; fi' script: - flake8 - - '[ "$TRAVIS_PYTHON_VERSION" != "pypy3" ] && black --check .' + - 'if [ "$TRAVIS_PYTHON_VERSION" != "pypy3" ]; then black --check .; fi' - pytest - make doc From 6e35ad94545ae272ef3051c56cbb6c99fa5db294 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 19:40:08 -0600 Subject: [PATCH 48/54] bump version to 1.0 --- docs/source/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index af4f3d9..02ed64b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,6 @@ master_doc = "index" project = u"Effect" copyright = u"2015, Christopher Armstrong" -version = release = "0.12.0+" +version = release = "1.0.0" html_theme = "sphinx_rtd_theme" diff --git a/setup.py b/setup.py index 5439395..ff74bda 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name="effect", - version="0.12.0+", + version="1.0.0", description="pure effects for Python", long_description=open("README.rst").read(), url="http://github.com/python-effect/effect/", From 3daf1828f5139bdff36bb7cbd438b8d8fd924c60 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 22:50:02 -0600 Subject: [PATCH 49/54] deprecate do_return. Should have just deleted it before releasing 1.0, but I forgot. Oh well! --- effect/_test_do_py3.py | 11 ----------- effect/do.py | 27 ++++++++++++++------------- effect/test_do.py | 33 +++++++++++++++++++++------------ effect/test_testing.py | 10 +++++----- effect/testing.py | 6 +++--- 5 files changed, 43 insertions(+), 44 deletions(-) delete mode 100644 effect/_test_do_py3.py diff --git a/effect/_test_do_py3.py b/effect/_test_do_py3.py deleted file mode 100644 index 1ac1df6..0000000 --- a/effect/_test_do_py3.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code only works in Python 3, so it's left out of test_do.py, to be -# optionally imported. - -from effect import Constant, Effect -from effect.do import do - - -@do -def py3_generator_with_return(): - yield Effect(Constant(1)) - return 2 # noqa diff --git a/effect/do.py b/effect/do.py index 966de3b..da7b995 100644 --- a/effect/do.py +++ b/effect/do.py @@ -5,6 +5,7 @@ """ import types +import warnings from . import Effect, Func from ._utils import wraps @@ -18,19 +19,15 @@ def do(f): @do def foo(): thing = yield Effect(Constant(1)) - yield do_return('the result was %r' % (thing,)) + return 'the result was %r' % (thing,) eff = foo() return eff.on(...) ``@do`` must decorate a generator function (not any other type of - iterator). Any yielded values must either be Effects or the result of a - :func:`do_return` call. The result of a yielded Effect will be passed back - into the generator as the result of the ``yield`` expression. Yielded - :func:`do_return` values will provide the ultimate result of the Effect - that is returned by the decorated function. Note that :func:`do_return` is - only necessary for Python 2 compatibility; return statements can be used - directly in Python 3-only code. + iterator). Any yielded values must be Effects. The result of a yielded Effect will be passed + back into the generator as the result of the ``yield`` expression. A returned value becomes the + ultimate result of the Effect that is returned by the decorated function. It's important to note that any generator function decorated by ``@do`` will no longer return a generator, but instead it will return an Effect, @@ -43,7 +40,7 @@ def foo(): try: thing = yield Effect(Error(RuntimeError('foo'))) except RuntimeError: - yield do_return('got a RuntimeError as expected') + return 'got a RuntimeError as expected' (This decorator is named for Haskell's ``do`` notation, which is similar in spirit). @@ -77,15 +74,19 @@ def do_return(val): """ Specify a return value for a @do function. + This is deprecated. Just use `return`. + The result of this function must be yielded. e.g.:: @do def foo(): yield do_return('hello') - - If you're writing Python 3-only code, you don't need to use this function, - and can just use the `return` statement as normal. """ + warnings.warn( + "do_return is deprecated. Just return as normal.", + DeprecationWarning, + stacklevel=1, + ) return _ReturnSentinel(val) @@ -119,6 +120,6 @@ def _do(result, generator, is_error): ) else: raise TypeError( - "@do functions must only yield Effects or results of do_return. " + "@do functions must only yield Effects. " "Got %r from %r" % (val, generator) ) diff --git a/effect/test_do.py b/effect/test_do.py index 8f308f0..60f5464 100644 --- a/effect/test_do.py +++ b/effect/test_do.py @@ -1,7 +1,7 @@ import sys from functools import partial -from py.test import raises +from py.test import raises, warns from . import ( ComposedDispatcher, @@ -34,19 +34,25 @@ def test_do_return(): """ When a @do function yields a do_return, the given value becomes the eventual result. + + This is deprecated. """ @do def f(): yield do_return("hello") - assert perf(f()) == "hello" + with warns(DeprecationWarning): + assert perf(f()) == "hello" -def test_do_return_effect(): +def test_return_effect(): @do def f(): - yield do_return(Effect(Constant("hello"))) + return Effect(Constant("hello")) + # this is a dumb trick we're playing on Python to make sure this function is a generator, + # even though we never want to yield anything. + yield assert perf(f()) == "hello" @@ -57,7 +63,7 @@ def test_yield_effect(): @do def f(): x = yield Effect(Constant(3)) - yield do_return(x) + return x perf(f()) == 3 @@ -83,8 +89,7 @@ def f(): with raises(TypeError) as err_info: perf(result) assert str(err_info.value).startswith( - "@do functions must only yield Effects or results of " - "do_return. Got 1 from Date: Sat, 23 Nov 2019 22:58:50 -0600 Subject: [PATCH 50/54] get rid of the very last bare-except --- effect/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effect/_utils.py b/effect/_utils.py index a5d96e8..574a91b 100644 --- a/effect/_utils.py +++ b/effect/_utils.py @@ -17,7 +17,7 @@ def wraps_decorator(wrapper): wrapper.__doc__ = original.__doc__ wrapper.__dict__.update(original.__dict__) wrapper.__module__ = original.__module__ - except: + except Exception: pass return wrapper From df04e119faf419a5608a51ca1af606f2874a11a7 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sat, 23 Nov 2019 23:02:42 -0600 Subject: [PATCH 51/54] bump to 1.1.0 --- docs/source/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 02ed64b..3cd32cf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,6 @@ master_doc = "index" project = u"Effect" copyright = u"2015, Christopher Armstrong" -version = release = "1.0.0" +version = release = "1.1.0" html_theme = "sphinx_rtd_theme" diff --git a/setup.py b/setup.py index ff74bda..b513d66 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name="effect", - version="1.0.0", + version="1.1.0", description="pure effects for Python", long_description=open("README.rst").read(), url="http://github.com/python-effect/effect/", From 8f51f4c0c307fb4bb1e3bdee7345780efb0372ef Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 21 Jul 2021 23:11:38 +1000 Subject: [PATCH 52/54] docs: Fix a few typos There are small typos in: - effect/_base.py - effect/fold.py - effect/test_base.py - effect/test_parallel_performers.py Fixes: - Should read `synchronously` rather than `synchronusly`. - Should read `successful` rather than `succesful`. - Should read `success` rather than `succes`. - Should read `propagate` rather than `propogate`. - Should read `performance` rather than `peformance`. --- effect/_base.py | 2 +- effect/fold.py | 2 +- effect/test_base.py | 4 ++-- effect/test_parallel_performers.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/effect/_base.py b/effect/_base.py index 6a4f145..efaef5c 100644 --- a/effect/_base.py +++ b/effect/_base.py @@ -163,7 +163,7 @@ def catch(exc_type, callable): lambda exc: "got an error!")) If any exception other than a ``SpecificException`` is thrown, it will be - ignored by this handler and propogate further down the chain of callbacks. + ignored by this handler and propagate further down the chain of callbacks. """ def catcher(error): diff --git a/effect/fold.py b/effect/fold.py index 65aa6ae..ec92e53 100644 --- a/effect/fold.py +++ b/effect/fold.py @@ -73,7 +73,7 @@ def sequence(effects): fails. """ # Could be: folder = lambda acc, el: acc + [el] - # But, for peformance: + # But, for performance: result = [] def folder(acc, el): diff --git a/effect/test_base.py b/effect/test_base.py index 68acc15..3b1d9c4 100644 --- a/effect/test_base.py +++ b/effect/test_base.py @@ -107,7 +107,7 @@ def test_performer_raises(self): def test_success_propagates_effect_exception(self): """ - If an succes callback is specified, but a exception result occurs, + If an success callback is specified, but a exception result occurs, the exception is passed to the next callback. """ calls = [] @@ -124,7 +124,7 @@ def test_success_propagates_effect_exception(self): def test_error_propagates_effect_result(self): """ - If an error callback is specified, but a succesful result occurs, + If an error callback is specified, but a successful result occurs, the success is passed to the next callback. """ calls = [] diff --git a/effect/test_parallel_performers.py b/effect/test_parallel_performers.py index e40129d..05b2dbf 100644 --- a/effect/test_parallel_performers.py +++ b/effect/test_parallel_performers.py @@ -21,7 +21,7 @@ class ParallelPerformerTestsMixin(object): def test_empty(self): """ When given an empty list of effects, ``perform_parallel_async`` returns - an empty list synchronusly. + an empty list synchronously. """ result = sync_perform(self.dispatcher, parallel([])) self.assertEqual(result, []) From 9c6ae147f5ea14ab6f2e906231d5fb9664de5904 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sun, 26 Jun 2022 11:39:59 -0500 Subject: [PATCH 53/54] Remove Rackspace thanks from README --- README.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.rst b/README.rst index 6b4f244..05ce89c 100644 --- a/README.rst +++ b/README.rst @@ -105,16 +105,6 @@ Some talks have been given about Effect. .. _`Functionalish Programming in Python with Effect`: https://www.youtube.com/watch?v=fM5d_2BS6FY .. _`Kiwi PyCon`: https://nzpug.org/ - -Thanks -====== - -Thanks to Rackspace for allowing me to work on this project, and having an -*excellent* `open source employee contribution policy`_ - -.. _`open source employee contribution policy`: https://www.rackspace.com/blog/rackspaces-policy-on-contributing-to-open-source/ - - Authors ======= From cd21859ad2babebcbf12fa372aef34b9cd25a10e Mon Sep 17 00:00:00 2001 From: Christopher Armstrong <227068+radix@users.noreply.github.com> Date: Sun, 26 Jun 2022 11:45:10 -0500 Subject: [PATCH 54/54] Freenode is extremely dead --- README.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.rst b/README.rst index 05ce89c..d485daf 100644 --- a/README.rst +++ b/README.rst @@ -124,12 +124,6 @@ but now has contributions from the following people: .. _`Tom Prince`: https://github.com/tomprince -IRC -=== - -There is a ``#python-effect`` IRC channel on irc.freenode.net. - - See Also ========