diff --git a/.circleci/config.yml b/.circleci/config.yml index ebc1839..307cbb2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ defaults: &defaults working_directory: ~/pfun docker: - - image: jonatkinson/python-poetry:3.7 + - image: circleci/python:3.7.5 version: 2 jobs: @@ -11,12 +11,18 @@ jobs: - checkout - restore_cache: key: dependency-cache-{{ checksum "poetry.lock" }} + - run: + name: Install poetry + command: POETRY_VERSION=1.0.9 curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - run: name: Configure poetry - command: poetry config settings.virtualenvs.in-project true + command: poetry config virtualenvs.in-project true - run: name: Install dependencies command: poetry install + - run: + name: Lint + command: poetry run pre-commit run --all - run: name: Test command: poetry run tox @@ -25,12 +31,6 @@ jobs: paths: - .venv - .tox - - run: - name: Lint - command: poetry run flake8 pfun tests examples - - run: - name: Typecheck - command: poetry run mypy pfun examples release: <<: *defaults steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5a501b2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,45 @@ +repos: + - repo: local + hooks: + - id: trailing-whitespace-fixer + name: trailing-whitespace-fixer + stages: [commit] + language: system + entry: trailing-whitespace-fixer + types: [text] + + - id: end-of-file-fixer + name: end-of-file-fixer + stages: [commit] + language: system + entry: end-of-file-fixer + types: [text] + + - id: isort + name: isort + stages: [commit] + language: system + entry: isort + types: [python] + + - id: yapf + name: yapf + stages: [commit] + language: system + entry: yapf + types: [python] + + - id: flake8 + name: flake8 + stages: [commit] + language: system + entry: flake8 + types: [python] + + - id: mypy + name: mypy + stages: [commit] + language: system + entry: mypy pfun + types: [python] + pass_filenames: false diff --git a/README.md b/README.md index e58b321..b3b8336 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,5 @@ Requires [poetry](https://poetry.eustace.io/) - Install dependencies with `poetry install` - Build documentation with `poetry run sphinx-build -b html docs/source docs/build` -- Run tests with `poetry run pytest` \ No newline at end of file +- Run tests with `poetry run pytest` +- Lint with `poetry run pre-commit --all` diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst index b445481..c61ee9f 100644 --- a/docs/source/api_reference.rst +++ b/docs/source/api_reference.rst @@ -1,7 +1,67 @@ API Reference -==== -Maybe ------ +============= +pfun +---- + +.. autoclass:: pfun.Immutable + :members: + + + +.. autoclass:: pfun.Dict + :members: + :special-members: + :exclude-members: __weakref__,clear,__setitem__,__delitem__ + +.. autoclass:: pfun.List + :members: + :special-members: + :exclude-members: __weakref__,clear,__setitem__,__delitem__ + + +.. autofunction:: pfun.curry + + +.. autofunction:: pfun.compose + + +.. autofunction:: pfun.always + + +.. autofunction:: pfun.pipeline + + +.. autofunction:: pfun.identity + + +pfun.effect +----------- +.. automodule:: pfun.effect + :members: + :imported-members: + +pfun.effect.files +----------------- +.. automodule:: pfun.effect.files + :members: + +pfun.effect.console +------------------- +.. automodule:: pfun.effect.console + :members: + +pfun.effect.subprocess +---------------------- +.. automodule:: pfun.effect.subprocess + :members: + +pfun.effect.ref +--------------- +.. automodule:: pfun.effect.ref + :members: + +pfun.maybe +---------- .. autofunction:: pfun.maybe.maybe .. autoattribute:: pfun.maybe.Maybe @@ -12,8 +72,8 @@ Maybe .. autoclass:: pfun.maybe.Nothing :members: -Either ------- +pfun.either +----------- .. autofunction:: pfun.either.either @@ -25,21 +85,21 @@ Either .. autoclass:: pfun.result.Left :members: -Result ------ +pfun.result +----------- .. autofunction:: pfun.result.result .. autoclass:: pfun.result.Result :members: -Reader ------- +pfun.reader +----------- .. automodule:: pfun.reader :members: -Writer ------- +pfun.writer +----------- .. automodule:: pfun.writer :members: @@ -47,24 +107,24 @@ Writer :exclude-members: __weakref__,__setattr__,__repr__ -State ------ +pfun.state +---------- .. automodule:: pfun.state :members: :special-members: :exclude-members: __weakref__,__setattr__,__repr__ -IO ----- +pfun.io +------- .. automodule:: pfun.io :members: :special-members: :exclude-members: __weakref__,__setattr__,__repr__ -Trampoline ----------- +pfun.trampoline +--------------- .. automodule:: pfun.trampoline :members: @@ -72,61 +132,17 @@ Trampoline :exclude-members: __weakref__,__setattr__,__repr__ -Cont ----- +pfun.cont +--------- .. automodule:: pfun.cont :members: :special-members: :exclude-members: __weakref__,__setattr__,__repr__ -Free ----- +pfun.free +--------- .. automodule:: pfun.free :members: :special-members: :exclude-members: __weakref__,__setattr__,__repr__ - - -Immutable ---------- - -.. autoclass:: pfun.Immutable - :members: - - -Dict ----- -.. autoclass:: pfun.Dict - :members: - :special-members: - :exclude-members: __weakref__,clear,__setitem__,__delitem__ -List ----- -.. autoclass:: pfun.List - :members: - :special-members: - :exclude-members: __weakref__,clear,__setitem__,__delitem__ - -curry ------ -.. autofunction:: pfun.curry - -compose -------- -.. autofunction:: pfun.compose - -always ------- -.. autofunction:: pfun.always - -pipeline --------- -.. autofunction:: pfun.pipeline - -identity --------- - -.. autofunction:: pfun.identity - - diff --git a/docs/source/conf.py b/docs/source/conf.py index cc6dd27..a04597b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,4 +10,5 @@ release = '0.5.1' source_suffix = ['.rst', '.md'] master_doc = 'index' -extensions = ['recommonmark', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] +extensions = ['recommonmark', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx_autodoc_typehints', 'sphinx_rtd_theme'] +html_theme = 'sphinx_rtd_theme' diff --git a/docs/source/programming_guide.md b/docs/source/programming_guide.md index 32aa479..aae7c5f 100644 --- a/docs/source/programming_guide.md +++ b/docs/source/programming_guide.md @@ -7,7 +7,7 @@ For a detailed documentation of all classes and functions, see [API Reference](a `pip install pfun` -### MyPy Plugin +## MyPy Plugin The types provided by the Python `typing` module are often not flexible enough to provide precise typing of common functional design patterns. If you use [mypy](http://mypy-lang.org/), `pfun` @@ -40,7 +40,7 @@ d.b = 2 # raises FrozenInstanceError ``` `Immutable` uses [dataclasses](https://docs.python.org/3/library/dataclasses.html) under the hood, so for detailed -usage documentation, see the official documentation. You can use the entire `dataclass` api. +usage documentation, see the official docs. You can use the entire `dataclass` api. ```python from dataclasses import field @@ -78,11 +78,11 @@ assert List(range(3)).map(str) == ['0', '1', '2'] `Dict` is a functional style dictionary. ```python -from pfun import Dict +from pfun import Dict, maybe d = Dict({'key': 'value'}) d2 = d.set('new_key', 'new_value') -assert 'new_key' not in d and d2['new_key'] == Just('new_value') +assert 'new_key' not in d and d2['new_key'] == maybe.Just('new_value') ``` It supports the same api as `dict` which the exception of `__setitem__` which will raise an exception, and uses @@ -123,7 +123,432 @@ assert f(1)(1) == 3 ``` ## Effectful (But Side-Effect Free) Functional Programming -### Maybe +In functional programming, programs are built by composing functions that have no side-effects. This means that things that we normally model as side-effects in imperative programming such as performing io or raising exceptions are modelled differently. The best way to deal with side-effecty things such as io or error handling with `pfun` is to use the `pfun.effect` module, which lets you work with side-effecty stuff in a side-effect free fashion. Readers with functional programming experience may be familiar with the term "[functional effect system](https://en.wikipedia.org/wiki/Effect_system)", which is precisely what `pfun.effect` is. + +`pfun` also offers more traditional ways of working with functional effects in the form of [MTL](https://github.com/haskell/mtl) style classes such as `pfun.maybe` or `pfun.reader`. We recommend using `pfun.effect` over these alternatives because composing multiple effects with MTL style classes (say `IO[Either]` for example) is cumbersome to use and type in MTL style, and effortless with `pfun.effect`. + +### Effect +The core type you will use when expressing side-effects with `pfun` is `pfun.effect.Effect`. `Effect` is a callable that + +- Takes exactly one argument +- May or may not perform side-effects when called (including raising exceptions) + +You can think of `Effect` defined as: +```python +from typing import TypeVar, Generic +from pfun.either import Either + + +R = TypeVar('R', contravariant=True) +E = TypeVar('E', covariant=True) +A = TypeVar('A', covariant=True) + + +class Effect(Generic[R, E, A]): + def __call__(self, r: R) -> A: + """ + May raise E + """ + ... +``` +In other words, `Effect` takes three type paramaters: `R`, `E` and `A`. We'll study them one at a time. + +#### The Success Type +The `A` in `Effect[R, E, A]` is the _success type_. This is the type that the effect function will return if no error occurs. For example, in an `Effect` instance that reads a file as a `str`, `A` would be parameterized with `str`. You can create an `Effect` instance that succeeds with the value `a` using `effect.success(a)`: + +```python +from typing import Any, NoReturn +from pfun import effect + +e: effect.Effect[Any, NoReturn, str] = effect.success('Success!') +assert e(None) == 'Success!' +``` +(You don't actually have to write the type of `e` explicitly, as it can be inferred by your type checker. We do it here simply because it's instructive to look at the types). Don't worry about the meaning of `Any` and `NoReturn` for now, we'll explain that later. For now, just understand that when `e` has the type `Effect[Any, NoReturn, str]`, it means that when you call `e` with any parameter, it will return a `str` (the value `Success!`). + +You can work with the success value of an effect using instance methods of `Effect`. If you want to transform the result of an `Effect` with a function without side-effects you can use `map`, which takes a function of the type `Callable[[A], B]` as an argument, where `A` is the success type of your effect: + +```python +e: effect.Effect[Any, NoReturn, str] = effect.success(1).map(str) +assert e(None) == "1" +``` + +If you want to transform the result of an `Effect` with a function that produces other side effects (that is, returns an `Effect` instance), you use `and_then`: +```python +add_1 = lambda v: effect.success(v + 1) +e: effect.Effect[Any, NoReturn, int] = effect.success(1).and_then(add_1) +assert e(None) == 2 +``` +(for those with previous functional programming experince, `and_then` is the "bind" operation of `Effect`). + + +#### The Error Type +The `E` in `Effect[R, E, A]` is the _error type_. This is type that the effect function will raise if it fails. You can create an effect that does nothing but fail using `pfun.effect.error`: + +```python +from typing import Any, NoReturn + +from pfun.effect import Effect, error + + +e: Effect[Any, str, NoReturn] = error('Whoops!') +e(None) # raises: RuntimeError('Whoops!') +``` + +For a concrete example, take a look at the `pfun.effect.files` module that helps you read from files: + +```python +from typing import Any + +from pfun.effect import Effect +from pfun.effect.files import Files + + +files = Files() +e: Effect[Any, OSError, str] = files.read('doesnt_exist.txt') +e(None) # raises OSError +``` +Don't worry about the api of `files` for now, simply notice that when `e` has the type `Effect[Any, OSError, str]`, it means that when you execute `e` it can produce a `str` or fail with `OSError`. Having the the error type explicitly modelled in the type signature of `e` allows type safe error handling as we'll see later. + +#### The Environment Type +Finally, let's look at `R` in `Effect[R, E, A]`: the _environment type_. `R` is the argument that your effect function requires to produce its result. It allows you to parameterize the side-effect that your `Effect` implements which improves re-useability and testability. For example, imagine that you want to use `Effect` to model the side-effect of reading from a database. The function that reads from the database requires a connection string as an argument to connect. If `Effect` did not take a parameter you would have to pass around the connection string as a parameter through function calls, all the way down to where the connection string was needed. + +The environment type allows you to pass in the connection string at the edge of your program, rather than threading it through a potentially deep stack of function calls: + +```python +from typing import List, Dict, Any + + +DBRow = Dict[Any, Any] + + +def execute(query: str) -> Effect[str, IOError, List[DBRow]]: + ... + +def find_row(results: List[DBRow]) -> DBRow: + ... + +def main() -> Effect[str, IOError, DBRow]: + return execute('select * from users;').map(find_row) + + +if __name__ == '__main__': + program = main() + + # run in production + program('user@prod_db') + + # run in development + program('user@dev_db') +``` +In the next section, we will discuss this _dependency injection_ capability of `Effect` in detail. + +#### The Module Pattern +This section is dedicated to the environment type `R`. In most examples we have looked at so far, `R` is parameterized with `typing.Any`. This means that it can safely be called with any value. This is mostly useful when you're working with effects that don't use the environment argument for anything, in which case any value will do. + +In the previous section we saw how the `R` parameter of `Effect` can be used for dependency injection. But what happens when we try to combine two effects with different environment types with `and_then`? The `Effect` instance returned by `and_then` must have an environment type that is a combination of the environment types of both the combined effects, since the environment passed to the combined effect is also passed to the two other effects. Consider for example this effect, that uses the `execute` function from above to get database results, and combines it with a function `make_request` that calls an api, and requires a `Credentials` instance as the environment type: + +```python +class Credentials: + ... + + +def make_request(results: List[DBRow]) -> Effect[Credentials, HTTPError, bytes]: + ... + +results: effect.Effect[str, IOError, List[DBRow]] = execute('select * from users;') +response: effect.Effect[..., Union[IOError, HTTPError], HTTPResponse] +response = results.and_then(make_request) +response(...) # What could this argument be? +``` +To call the `response` function, we need an instance of a type that is a `str` and a `Credentials` instance _at the same time_, because that argument must be passed to both the effect returned by `execute` and by `make_request`. Ideally, we want `response` to have the type `Effect[Intersection[Credentials, str], IOError, bytes]`, where `Intersection[Credentials, str]` indicates that the environment type must be both of type `Credentials` and of type `str`. + +In theory such an object could exist (defined as `class MyEnv(Credentials, str): ...`), but there are no straight-forward way of expressing that +type dynamically in the Python type system. As a consequence, `pfun` infers the resulting effect with the `R` parameterized as `Any`, which in this case doesn't mean that any type will do, but simply that `pfun` could not assign a meaningful type to `R`. + +If you use the `pfun` MyPy plugin, you can however redesign the program to follow a pattern that enables `pfun` to infer a meaningful combined type +in much the same way that the error type resulting from combining two effects using `and_then` can be inferred. This pattern is called _the module pattern_. + +In its most basic form, the module pattern simply involves defining a [Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol) that serves as the environment type of an `Effect`. `pfun` can combine environment types of two effects whose environment types are both protocols, because the combined environment type is simply a new protocol that inherits from both. This combined protocol is called `pfun.effect.Intersection`. + +In many cases the api for effects involved in the module pattern is split into three parts: + +- A _module_ class that provides the actual implementation +- A _module provider_ that is a `typing_extensions.Protocol` that provides the module class as an attribute +- Functions that return effects with the module provider class as the environment type. + +Lets rewrite our example from before to follow the module pattern: +```python +from typing import Any +from http.client import HTTPError +from typing_extensions import Protocol + +from pfun.effect import Effect, get_environment + +class Requests: + """ + Requests implementation module + """ + def __init__(self, credentials: Credentials): + self.credentials = credentials + + def make_request(self, results: List[DBRow]) -> Effect[Any, HTTPError, bytes]: + ... + + +class HasRequests(Protocol): + """ + Module provider class for the requests module + """ + requests: Requests + + +def make_request(results: List[DBRow]) -> Effect[HasRequests, HTTPError, bytes]: + """ + Function that returns an effect with the HasRequest module provider as the environment type + """ + return get_environment().and_then(lambda env: env.requests.make_request(results)) + + +class Database: + """ + Database implementation module + """ + def __init__(self, connection_str: str): + self.connection_str = connection_str + + def execute(self, query: str) -> Effect[Any, IOError, List[DBRow]]: + ... + + +class HasDatabase(Protocol): + """ + Module provider class for the database module + """ + database: Database + + +def execute(query: str) -> Effect[HasDatabase, IOError, List[DBRow]]: + """ + Function that returns an effect with the HasDatabase module provider as the environment type + """ + return get_environment().and_then(lambda env: env.database.execute(query)) +``` +There are two _modules_: `Requests` and `Database` that provide implementations. There are two corresponding _module providers_: `HasRequests` and `HasDatabase`. Finally there are two functions `execute` and `make_request` that puts it all together. + +Pay attention to the fact that `execute` and `make_request` look quite similar: they both start by calling `pfun.effect.get_environment`. This function returns an effect that succeeds with the environment value that will eventually be passed as the argument to the final effect (in this example the effect produced by `execute(...).and_then(make_request)`). If you use the MyPy plugin, `pfun` is able to infer the return type of `get_environment` in the body definition of a function that returns an `Effect` instance. For example, in the function body of `execute`, `pfun` is able to infer that the return type of `get_environment` must be `Effect[HasRequests, NoReturn, HasRequests]`. + +If we combine the new functions `execute` and `make_request` that both has protocols as the environment types, `pfun` can infer a meaningful type, and make sure that the environment type that is eventually passed to the whole program provides both the `requests` and the `database` attributes: + +```python +effect = execute('select * from users;').and_then(make_request) +``` +The type of `effect` in this case will be +```python +Effect[ + pfun.effect.Intersection[HasRequests, HasDatabase], + Union[HTTPError, IOError], + bytes +] +``` + +Quite a mouthful, but what it tells us is that `effect` must be called with an instance of a type that has both the `requests` and `database` attributes with appropriate types. In other words, if you accidentally defined your environment as: +```python +class Env: + database = Database('user@prod_db') + + +effect(Env()) +``` +MyPy would tell you the call `effect(Env())` is a type error since `Env` does not have a `requests` attribute. It's worth understanding the module pattern, since `pfun` uses it pervasively in its api, e.g in `pfun.effect.files` and `pfun.effect.console`, in order that `pfun` can infer the environment type of effects resulting from combining functions from `pfun` with user defined functions that also follow the module pattern. + +A very attractive added bonus of the module pattern is that mocking out particular dependencies of your program becomes extremely simple, and by extension that unit testing becomes easier: +```python +from pfun.effect import success +from unittest.mock import Mock + + +mock_env = Mock() +mock_env.requests.make_request.return_value = success(b'Mocked!') + +assert make_request([])(mock_env) == b'Mocked!' +``` + + +#### Error Handling +In this section, we'll look at how to handle errors of effects with type safety. In previous sections we have already spent some time looking at the `Effect` error type. In many of the examples so far, the error type was `typing.NoReturn`. An `Effect` with this error type can never return a value for an error, or in other words, it can never fail (as those effects returned by `pfun.effect.success`). In the rest of this section we'll of course be pre-occupied with effects that _can_ fail. + +When you combine side effects using `Effect.and_then`, `pfun` uses `typing.Union` to combine error types, in order that the resulting effect captures all potential errors in its error type: +```python +from typing import List + +from pfun.effect.files import Files + + +def parse(content: str) -> effect.Effect[Any, ZeroDivisionError, List[int]]: + ... + + +files = Files() +e: effect.Effect[Any, Union[OSError, ZeroDivisionError], List[int]] +e = files.read('foo.txt').and_then(parse) +``` +`e` has `Union[OSError, ZeroDivisionError]` as its error type because it can fail if `files.read` fails, _or_ if `parse` fails. This compositional aspect of the error type of `Effect` means that accurate and complex error types are built up from combining simple error types. Moreover, it makes reasoning about error handling easy because errors disappear from the type when they are handled, as we shall see next. + +The most low level function you can use to handle errors is `Effect.either`, which surfaces any errors that may have occurred as a `pfun.either.Either`, where a `pfun.either.Right` signifies a successful computation and a `pfun.either.Left` a failed computation: +```python +from typing import NoReturn +from pfun.effect import Effect, files +from pfun.either import Either, Left + + +# files.read can fail with OSError +may_have_failed: Effect[files.HasFiles, OSError, str] = files.read('foo.txt') +# calling either() surfaces the OSError in the success type as a pfun.either.Either +as_either: Effect[files.HasFiles, NoReturn, Either[OSError, str]] = may_have_failed.either() +# we can use map or and_then to handle the error +cant_fail: Effect[files.HasFiles, NoReturn, str] = as_either.map(lambda either: 'backup content' if isinstance(either, Left) else either.get) +``` + +Once you've handled whatever errors you want, you can push the error back into error type of the effect using `pfun.effect.absolve`: +```python +from typing import Any, NoReturn, List +from pfun.effect import Effect, absolve, files +from pfun.either import Either + + +# function to handle error +def handle(either: Either[Union[OSError, ZeroDivisionError], str]) -> Either[ZeroDivisionError, str]: + ... + +# define an effect that can fail +e: Effect[Any, Union[OSError, ZeroDivisionError], List[int]] = files.read('foo.txt').and_then(parse) +# handle errors using e.either.map +without_os_error: Effect[Any, NoReturn, Either[OSError, str]] = e.either().map(handle) +# push the remaining error into the error type using absolve +e2: Effect[Any, OSError, str] = absolve(without_os_error) +``` + +At a slightly higher level, you can use `Effect.recover`, which takes a function that can inspect the error and handle it. +```python +from typing import Any, Union +from pfun.effect import success, failure, Effect + + +def handle_errors(error: Union[OSError, ZeroDivisionError]) -> Effect[Any, ZeroDivisionError, str]: + if isinstance(error, OSError): + return success('default value) + return failure(error) + +e: Effect[Any, Union[OSError, ZeroDivisionError], str] +recovered: Effect[Any, ZeroDivisionError, str] = e.recover(handle_errors) +``` + +You will frequently handle errors by using `isinstance` to compare errors with types, so defining your own error types becomes even more important when using `pfun` to distinguish one error source from another. + +#### Asynchronous IO +`Effect` uses `asyncio` under the hood to run io bound side-effects asynchronously when possible. +This can lead to significant speed ups when an effect spends alot of time waiting for io. + +Consider for example this program that calls `curl http://www.google.com` in a subprocess 50 times: +```python +# call_google_sync.py +import timeit +import subprocess + +[subprocess.run(['curl', 'http://www.google.com']) for _ in range(50)] +``` +Timing the execution using the unix `time` informs me this takes 5.15 seconds on my computer. Compare this to the program below which does more or less the same thing, but using `pfun.effect.subprocess`: + +```python +# call_google_async.py +from pfun.effect.subprocess import Subprocess +from pfun.effect import sequence_async + + +sp = Subprocess() +effect = sequence_async(sp.run_in_shell('curl http://www.google.com') for _ in range(50) +effect(None) +``` + +This program finishes in 0.78 seconds, according to `time`. The crucial difference is the function `pfun.effect.sequence_async` which returns a new effect that runs its argument effects asynchronously using `asyncio`. This means that one effect can yield to other effects while waiting for input from the `curl` subprocess. This ultimately saves a lot of time compared to the synchronous implementation where each call to `subprocess.run` can only start when the preceeding one has returned. Functions that combine several effects such as `pfun.effect.filter_m` or `pfun.effect.map_m` generally run effects asynchronously, meaning you don't have to think too much about it. + +You can create an effect from a Python awaitable using `pfun.effect.from_awaitable`, allowing you to integrate with `asyncio` directly in your own code: +```python +import asyncio +from typing import Any, NoReturn +from pfun.effect import from_awaitable, Effect + + +async def sleep() -> str: + await asyncio.sleep(1) + return 'success!' + + +e: Effect[Any, NoReturn, str] = effect.from_awaitable(sleep()) +assert e(None) == 'success!' +``` + +You can also pass `async` functions directly to `map` and `and_then`: +```python +from typing import Any, NoReturn +import asyncio + +from pfun.effect import success + + +async def sleep_and_add_1(a: int) -> int: + await asyncio.sleep(1) + return a + 1 + + +assert success(1).map(sleep_and_add_1)(None) == 2 +``` + +#### Purely Functional State +Mutating non-local state is a side-effect that we want to avoid when doing functional programming. This means that we need a mechanism for managing state as an effect. `pfun.effect.ref` provides exactly this. `pfun.effect.ref` works by mutating state only by calling `Effect` instances. + +```python +from typing import Tuple, Any, NoReturn + +from pfun.effect.ref import Ref +from pfun.effect import Effect + + +ref: Ref[Tuple[int, ...]] = Ref(()) +add_1: Effect[Any, NoReturn, None] = ref.modify(lambda old: return old + (1,)) +# calling modify doesn't modify the state directly +assert ref.value == () +# The state is modified only when the effect is called +add_1(None) +assert ref.value == (1,) +``` +`pfun.effect.ref.Ref` protects access to the state using an `asyncio.Lock`, meaning that updating the state can be done atomically with the following methods: +- `Ref.get()` read the current value of the state +- `Ref.set(new_state)` update the state to `new_value` atomically, meaning no other effect can read the value of the state while the update is in progress. Note that if you first read the state using `Ref.get` and then set it with `Ref.set`, other effects may read the value in between which may lead to lost updates. _For this use case you should use `modify` or `try_modify`_ +- `Ref.modify(update_function)` read and update the state with `update_function` atomically, meaning no other effect can read or write the state before the effect produced by `modify` returns +- `Ref.try_modify(update_function)` read and update the state with `update_function` atomically, if `update_funciton` succeeds. Success is signaled by the `update_function` by returning a `pfun.either.Right` instance, and error by returning a `pfun.either.Left` instance. + +`pfun.effect.ref` can of course be combined with the module pattern: +```python +from typing import Tuple, Any, NoReturn +from typing_extensions import Protocol + +from pfun.effect.ref import Ref +from pfun.effect import get_environment, Effect + + +class HasState(Protocol): + state: Ref[Tuple[int, ...]] + + +def set_state(state: Tuple[int, ...]) -> Effect[HasState, NoReturn, None]: + return get_environment().and_then(lambda env.state.set(state)) +``` + +### MTL Style Effect Types +In the following sections we will look at more traditional alternatives to working with `pfun.effect`. As already stated, we recommend using `pfun.effect` over these classes, but for some use-cases, all the features of `pfun.effect` might be overkill, and one or more of the following types in combination may be more appropriate. + +#### Maybe Say you have a function that can fail: ```python @@ -172,7 +597,7 @@ The only requirement for the function argument to `and_then` is that it returns monadic type that you started with (a `Maybe` in this case). A function that returns a monadic value is called a _monadic function_. -### Either +#### Either `Maybe` allowed us to put the failure effect in the type signature, but it doesn't tell the caller _what_ went wrong. `Either` will do that: @@ -186,7 +611,7 @@ def i_can_fail(s: str) -> Either[ValueError, str]: return Right('Ok!') ``` -### Reader +#### Reader Imagine that you're trying to write a Python program in functional style. In many places in your code, you need to instantiate dependencies (like a database connection). You could of course instantiate that @@ -251,7 +676,7 @@ def main(): ``` -### Writer +#### Writer Imagine that you are logging by appending to a `tuple` (Why a `tuple`? Well because they're immutable of course!). Trying to avoid global mutable state, you decide to pass the list around as a common argument to all the functions @@ -262,7 +687,7 @@ def i_need_to_log_something(i: int, log: Tuple[str]) -> Tuple[int, Tuple[str]]: result = compute_something(i) log = log + ('Something was successfully computed',) return result, log - + def i_need_to_log_something_too(i: int, log: Tuple[str]) -> Tuple[int, Tuple[str]]: result = compute_something_else(i) log = log + ('Something else was computed',) @@ -286,8 +711,8 @@ from pfun.writer import value, Writer def i_need_to_log_something(i: int) -> Writer[int, List[str]]: result = compute_something(i) return Writer(result, ['Something was successfully computed']) - - + + def i_need_to_log_something_too(i: int) -> Writer[int, List[str]]: result = compute_something_else(i) return Writer(result, ['Something else was successfully computed']) @@ -296,12 +721,12 @@ def i_need_to_log_something_too(i: int) -> Writer[int, List[str]]: def main(): _, log = i_need_to_log_something(1).and_then(i_need_to_log_something_too) print('log', log) # output: ['Something was successfully computed', 'Something else was successfully computed'] - + ``` `tuple` is not the only thing `Writer` can combine: in fact the only requirement on the second argument is that its a _monoid_. You can even tell writer to combine custom types by implementing the `Monoid` ABC. -### State +#### State Where `Reader` can only read the context passed into it, and `Writer` can only append to a monoid but not read it, `State` can do both. You can use it to thread some state through a computation without global shared state @@ -322,7 +747,7 @@ state = add('first element').and_then( print(state.run(())) # outputs (None, ('second element',)) ``` The `None` value is the result of the computation (which is nothing, because all we do is change the state), and `('second element',)` is the final state. -### IO +#### IO A program that can't interact with the outside world isn't much use. But how can we keep our program pure and still interact with the outside world? The common solution is to use `IO` to separate the pure parts of our program from the unpure parts @@ -339,8 +764,8 @@ print_.run() with `map` and `and_then` just like the other monads we have seen. -### Combining Monadic Values -Sometimes you want to combine multiple unwrapped monadic values +#### Combining Monadic Values +Sometimes you want to combine multiple unwrapped monadic values like in the `get_full_name` function below: ```python from pfun.maybe import Just, Maybe @@ -442,17 +867,17 @@ to put the recursive call in tail-call position. ```python def factorial(n: int) -> int: - + def factorial_acc(n: int, acc: int) -> int: if n == 1: return acc - return factorial_acc(n - 1; n * acc) - + return factorial_acc(n - 1, n * acc) + return factorial_acc(n, 1) ``` In Python however, this is not enough to solve the problem because Python does not perform tail-call-optimization. -In languages without tail-call-optimization such as Python, its common to use a data structure called a trampoline +Because Python doesn't optimize tail calls, we need to use a data structure called a trampoline to wrap the recursive calls into objects that can be interpreted in constant stack space, by letting the function return immediately at each recursive step. @@ -461,7 +886,7 @@ from pfun.trampoline import Trampoline, Done, Call def factorial(n: int) -> int: - + def factorial_acc(n: int, acc: int) -> Trampoline[int]: if n == 1: return Done(acc) @@ -484,7 +909,7 @@ way of solving recursive problems (when it doesn't break [referential transparen is often easier to understand. Sometimes you'll find yourself in a situation where you want to write a recursive monadic function. -For some monads this is not a problem since they are designed to be stack safe (`Reader`, `State`, `IO`, and `Cont`). +For some monads this is not a problem since they are designed to be stack safe (`Effect`, `Reader`, `State`, `IO`, and `Cont`). But for other monads (`Maybe`, `Either` and `Writer`), this can lead to `RecursionError`. Consider `pow_writer` which computes integer powers by recursion: ```python @@ -496,7 +921,7 @@ def pow_writer(n: int, m: int) -> Writer[None, int]: return tell(n).and_then(lambda _: pow_writer(n, m - 1)) ``` -`pow_writer` cannot easily be trampolined because the function passed to `and_then` which performs the recursion +`pow_writer` cannot easily be trampolined because the function passed to `and_then` which performs the recursion must return a `Writer`, and not a `Trampoline`. In these cases the helper function `tail_rec` is provided which can help you trampoline you monadic function using `Either`: diff --git a/examples/free_kvstore.py b/examples/free_kvstore.py index 75e150a..fbb3b0a 100644 --- a/examples/free_kvstore.py +++ b/examples/free_kvstore.py @@ -1,16 +1,17 @@ -from pfun.free import ( - Functor, Free, More, Done, FreeInterpreter, FreeInterpreterElement -) -from pfun import Immutable, Dict, compose -from pfun.state import State, get as get_, put as put_ +from typing import Callable, TypeVar -from typing import TypeVar, Callable +from pfun import Dict, Immutable, compose +from pfun.free import (Done, Free, FreeInterpreter, FreeInterpreterElement, + Functor, More) +from pfun.state import State +from pfun.state import get as get_ +from pfun.state import put as put_ A = TypeVar('A') B = TypeVar('B') -class KVStoreF(Functor, Immutable): # type: ignore +class KVStoreF(Functor, Immutable): pass @@ -23,7 +24,7 @@ class KVStoreF(Functor, Immutable): # type: ignore set_state: Callable[[KVStore], KVStoreInterpreterState[None]] = put_ -class Put(KVStoreF, KVStoreElement): # type: ignore +class Put(KVStoreF, KVStoreElement): k: str v: str a: KVStoreFree @@ -39,7 +40,7 @@ def accept( ).and_then(lambda _: interpreter.interpret(self.a)) -class Get(KVStoreF, KVStoreElement): # type: ignore +class Get(KVStoreF, KVStoreElement): key: str h: Callable[[str], KVStoreFree] @@ -57,7 +58,7 @@ def accept( ) # yapf: disable -class Delete(KVStoreF, KVStoreElement): # type: ignore +class Delete(KVStoreF, KVStoreElement): key: str a: KVStoreFree diff --git a/examples/transaction_reader.py b/examples/transaction_reader.py index b1f5b23..d520595 100644 --- a/examples/transaction_reader.py +++ b/examples/transaction_reader.py @@ -1,7 +1,7 @@ -from pfun.reader import Reader, value, ask -from pfun import Immutable, curry +from typing import Callable, List, TypeVar -from typing import TypeVar, List, Callable +from pfun import Immutable, curry +from pfun.reader import Reader, ask, value class Transaction: @@ -18,7 +18,7 @@ def rollback(self) -> bool: ... -class User(Immutable): # type: ignore +class User(Immutable): user_id: int password: str diff --git a/logo/pfun_logo.svg b/logo/pfun_logo.svg index c1f769b..63ba957 100644 --- a/logo/pfun_logo.svg +++ b/logo/pfun_logo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/pfun/__init__.py b/pfun/__init__.py index 1b7e6cb..2ad7d34 100644 --- a/pfun/__init__.py +++ b/pfun/__init__.py @@ -1,16 +1,11 @@ # flake8: noqa -from . import maybe -from . import reader -from . import writer -from . import state -from . import result -from . import io -from .util import (identity, compose, pipeline, Unary, Predicate, always) -from .dict import Dict -from .list import List +from . import io, maybe, reader, result, state, writer from .curry import curry +from .dict import Dict from .immutable import Immutable +from .list import List +from .util import Predicate, Unary, always, compose, identity, pipeline __all__ = [ 'maybe', diff --git a/pfun/aio_trampoline.py b/pfun/aio_trampoline.py new file mode 100644 index 0000000..bd61eb6 --- /dev/null +++ b/pfun/aio_trampoline.py @@ -0,0 +1,145 @@ +from abc import ABC, abstractmethod +from asyncio import iscoroutine +from typing import Awaitable, Callable, Generic, Iterable, TypeVar, Union, cast + +from .immutable import Immutable +from .monad import Monad, sequence_ + +A = TypeVar('A', covariant=True) +B = TypeVar('B') +C = TypeVar('C') + + +class Trampoline(Immutable, Monad, Generic[A], ABC): + """ + Base class for Trampolines. Useful for writing stack safe-safe + recursive functions. + """ + @abstractmethod + async def _resume(self) -> 'Trampoline[A]': + pass + + @abstractmethod + async def _handle_cont( + self, cont: Callable[[A], 'Trampoline[B]'] + ) -> 'Trampoline[B]': + pass + + @property + def _is_done(self) -> bool: + return isinstance(self, Done) + + def and_then(self, f: Callable[[A], 'Trampoline[B]']) -> 'Trampoline[B]': + """ + Apply ``f`` to the value wrapped by this trampoline. + + :param f: function to apply the value in this trampoline + :return: Result of applying ``f`` to the value wrapped by \ + this trampoline + """ + return AndThen(self, f) + + def map(self, f: Callable[[A], B]) -> 'Trampoline[B]': + """ + Map ``f`` over the value wrapped by this trampoline. + + :param f: function to wrap over this trampoline + :return: new trampoline wrapping the result of ``f`` + """ + return self.and_then(lambda a: Done(f(a))) # type: ignore + + async def run(self) -> A: + """ + Interpret a structure of trampolines to produce a result + + :return: result of intepreting this structure of \ + trampolines + """ + trampoline = self + while not trampoline._is_done: + trampoline = await trampoline._resume() + + return cast(Done[A], trampoline).a + + +class Done(Trampoline[A]): + """ + Represents the result of a recursive computation. + """ + a: A + + async def _resume(self) -> Trampoline[A]: + return self + + async def _handle_cont( + self, + cont: Callable[[A], Union[Awaitable[Trampoline[B]], Trampoline[B]]] + ) -> Trampoline[B]: + result = cont(self.a) + if iscoroutine(result): + return await result # type: ignore + return result # type: ignore + + +class Call(Trampoline[A]): + """ + Represents a recursive call. + """ + thunk: Callable[[], Awaitable[Trampoline[A]]] + + async def _handle_cont(self, cont: Callable[[A], Trampoline[B]] + ) -> Trampoline[B]: + trampoline = await self.thunk() # type: ignore + return trampoline.and_then(cont) + + async def _resume(self) -> Trampoline[A]: + return await self.thunk() # type: ignore + + +class AndThen(Generic[A, B], Trampoline[B]): + """ + Represents monadic bind for trampolines as a class to avoid + deep recursive calls to ``Trampoline.run`` during interpretation. + """ + sub: Trampoline[A] + cont: Callable[[A], Union[Trampoline[B], Awaitable[Trampoline[B]]]] + + async def _handle_cont(self, cont: Callable[[B], Trampoline[C]] + ) -> Trampoline[C]: + return self.sub.and_then(self.cont).and_then(cont) # type: ignore + + async def _resume(self) -> Trampoline[B]: + return await self.sub._handle_cont(self.cont) # type: ignore + + def and_then( # type: ignore + self, f: Callable[[A], Trampoline[B]] + ) -> Trampoline[B]: + def cont(x): + async def thunk(): + t = self.cont(x) + if iscoroutine(t): + print('awaiting') + t = await t + return t.and_then(f) + + return Call(thunk) + + return AndThen(self.sub, cont) + + +def sequence(iterable: Iterable[Trampoline[A]]) -> Trampoline[Iterable[A]]: + """ + Evaluate each :class:`Trampoline` in `iterable` from left to right + and collect the results + + :example: + >>> sequence([Just(v) for v in range(3)]) + Just((0, 1, 2)) + + :param iterable: The iterable to collect results from + :returns: ``Trampoline`` of collected results + """ + return cast(Trampoline[Iterable[A]], sequence_(Done, iterable)) + + +__all__ = ['Trampoline', 'Done', 'sequence', 'Call', 'AndThen'] diff --git a/pfun/cont.py b/pfun/cont.py index 9b43f46..a2d0b93 100644 --- a/pfun/cont.py +++ b/pfun/cont.py @@ -1,9 +1,9 @@ -from typing import Generic, Callable, TypeVar, Iterable, cast, Generator +from typing import Callable, Generator, Generic, Iterable, TypeVar, cast -from .immutable import Immutable -from .monad import Monad, sequence_, map_m_, filter_m_ from .curry import curry -from .trampoline import Trampoline, Done, Call +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ +from .trampoline import Call, Done, Trampoline from .util import identity from .with_effect import with_effect_ @@ -13,7 +13,7 @@ D = TypeVar('D') -class Cont(Generic[A, B], Monad, Immutable): # type: ignore +class Cont(Generic[A, B], Monad, Immutable): """ Type that represents a function in continuation passing style. """ diff --git a/pfun/curry.py b/pfun/curry.py index bd46d6e..d07925d 100644 --- a/pfun/curry.py +++ b/pfun/curry.py @@ -1,11 +1,11 @@ -from typing import Callable import functools import inspect +from typing import Callable from .immutable import Immutable -class Curry(Immutable): # type: ignore +class Curry(Immutable): f: Callable def __repr__(self): diff --git a/pfun/dict.py b/pfun/dict.py index 12175bf..5cb78a6 100644 --- a/pfun/dict.py +++ b/pfun/dict.py @@ -1,23 +1,15 @@ -from typing import ( - TypeVar, - Dict as Dict_, - Union, - Mapping, - KeysView, - ValuesView, - ItemsView, - Generic, - Iterator -) - -from .maybe import Maybe, Nothing, Just +from typing import Dict as Dict_ +from typing import (Generic, ItemsView, Iterator, KeysView, Mapping, TypeVar, + Union, ValuesView) + from .immutable import Immutable +from .maybe import Just, Maybe, Nothing K = TypeVar('K') V = TypeVar('V') -class Dict(Immutable, Generic[K, V], init=False): # type: ignore +class Dict(Immutable, Generic[K, V], init=False): """ Immutable dictionary class with functional helper methods """ diff --git a/pfun/effect/__init__.py b/pfun/effect/__init__.py new file mode 100644 index 0000000..8945923 --- /dev/null +++ b/pfun/effect/__init__.py @@ -0,0 +1,2 @@ +from . import console, files, ref, subprocess # noqa +from .effect import * # noqa diff --git a/pfun/effect/console.py b/pfun/effect/console.py new file mode 100644 index 0000000..8e3b58a --- /dev/null +++ b/pfun/effect/console.py @@ -0,0 +1,97 @@ +import asyncio +from typing import Any, NoReturn + +from typing_extensions import Protocol + +from ..aio_trampoline import Done, Trampoline +from ..either import Either, Right +from ..immutable import Immutable +from .effect import Effect, get_environment + + +class Console(Immutable): + """ + Module that enables printing to stdout and reading from stdin + """ + def print(self, msg: str = '') -> Effect[Any, NoReturn, None]: + """ + Get an effect that prints to stdout + + :example: + >>> Console().print('Hello pfun!').run(None) + Hello pfun! + + :param msg: Message to print + :return: :class:`Effect` that prints `msg` to stdout + """ + async def run_e(_) -> Trampoline[Either[NoReturn, None]]: + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, print, msg) + return Done(Right(None)) + + return Effect(run_e) + + def input(self, prompt: str = '') -> Effect[Any, NoReturn, str]: + """ + Get an effect that reads from stdin + + :example: + >>> greeting = lambda name: f'Hello {name}' + >>> Console().input('What is your name? ').map(greeting).run(None) + what is your name? # input e.g "John Doe" + 'Hello John Doe!' + + :param prompt: Prompt to dislay on stdout + :return: :class:`Effect` that reads from stdin + """ + async def run_e(_): + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(None, input, prompt) + return Done(Right(result)) + + return Effect(run_e) + + +class HasConsole(Protocol): + """ + Module provider providing the `console` module + + :type console: Console + :attribute console: The instance of the console + """ + console: Console + + +def print_line(msg: str = '') -> Effect[HasConsole, NoReturn, None]: + """ + Get an :class:`Effect` that prints to the console and succeeds with `None` + + :example: + >>> class Env: + ... console = Console() + >>> print_line('Hello pfun!').run(Env()) + Hello pfun! + + :param msg: Message to print + :return: :class:`Effect` that prints to the console using the \ + :class:`HasConsole` provided to `run` + """ + return get_environment().and_then(lambda env: env.console.print(msg)) + + +def get_line(prompt: str = '') -> Effect[HasConsole, NoReturn, str]: + """ + Get an :class:`Effect` that reads a `str` from stdin + + :example: + >>> class Env: + ... console = Console() + >>> greeting = lambda name: f'Hello {name}!' + >>> get_line('What is your name? ').map(greeting).run(Env()) + name? # input e.g 'John Doe' + 'Hello John Doe!' + + :param prompt: prompt to display in console + :return: an :class:`Effect` that produces a `str` read from stdin + """ + return get_environment().and_then(lambda env: env.console.input(prompt)) diff --git a/pfun/effect/effect.py b/pfun/effect/effect.py new file mode 100644 index 0000000..e208081 --- /dev/null +++ b/pfun/effect/effect.py @@ -0,0 +1,524 @@ +from __future__ import annotations + +import asyncio +from functools import wraps +from typing import (Any, Awaitable, Callable, Generator, Generic, Iterable, + NoReturn, Type, TypeVar, Union) + +from ..aio_trampoline import Call, Done, Trampoline +from ..aio_trampoline import sequence as sequence_trampolines +from ..curry import curry +from ..either import Either, Left, Right +from ..either import sequence as sequence_eithers +from ..immutable import Immutable + +R = TypeVar('R', contravariant=True) +E = TypeVar('E', covariant=True) +E2 = TypeVar('E2') +A = TypeVar('A', covariant=True) +B = TypeVar('B') + + +class Effect(Generic[R, E, A], Immutable): + """ + Wrapper for functions that are allowed to perform side-effects + """ + run_e: Callable[[R], Awaitable[Trampoline[Either[E, A]]]] + + def and_then( + self, + f: Callable[[A], + Union[Awaitable[Effect[Any, E2, B]], Effect[Any, E2, B]]] + ) -> Effect[Any, Union[E, E2], B]: + """ + Create new :class:`Effect` that applies ``f`` to the result of \ + running this effect successfully. If this :class:`Effect` fails, \ + ``f`` is not applied. + + :example: + >>> success(2).and_then(lambda i: success(i + 2)).run(None) + 4 + + :param f: Function to pass the result of this :class:`Effect` \ + instance once it can be computed + + :return: New :class:`Effect` which wraps the result of \ + passing the result of this :class:`Effect` instance to ``f`` + """ + async def run_e(r: R) -> Trampoline[Either[Union[E, E2], B]]: + async def thunk(): + def cont(either: Either): + if isinstance(either, Left): + return Done(either) + + async def thunk(): + next_ = f(either.get) + if asyncio.iscoroutine(next_): + effect = await next_ + else: + effect = next_ + return await effect.run_e(r) + + return Call(thunk) + + trampoline = await self.run_e(r) + return trampoline.and_then(cont) + + return Call(thunk) + + return Effect(run_e) + + def discard_and_then(self, effect: Effect[Any, E2, B] + ) -> Effect[Any, Union[E, E2], B]: + """ + Create a new effect that discards the result of this effect, \ + and produces instead ``effect``. Like ``and_then`` but does not require + you to handle the result. \ + Convenient for effects that produce ``None``, like writing to files. + + :example: + >>> class Env: + ... files = effect.files.Files() + >>> effect.files.write('foo.txt', 'Hello!')\\ + ... .discard_and_then(effect.files.read('foo.txt'))\\ + ... .run(Env()) + Hello! + + :param effect: :class:`Effect` instance to run after this \ + :class:`Effect` has run successfully. + + :return: New effect that succeeds with `effect` + """ + return self.and_then(lambda _: effect) # type: ignore + + def either(self) -> Effect[R, NoReturn, Either[E, A]]: + """ + Push the potential error into the success channel as an either, \ + allowing error handling. + + :example: + >>> error('Whoops!').either().map( + ... lambda either: either.get if isinstance(either, Right) + ... else 'Phew!' + ... ).run(None) + 'Phew!' + + :return: New :class:`Effect` that produces a :class:`Left[E]` if it \ + has failed, or a :class:`Right[A]` if it succeeds + """ + async def run_e(r: R) -> Trampoline[Either[NoReturn, Either[E, A]]]: + async def thunk() -> Trampoline[Either[NoReturn, Either[E, A]]]: + trampoline = await self.run_e(r) # type: ignore + return trampoline.and_then(lambda either: Done(Right(either))) + + return Call(thunk) + + return Effect(run_e) + + def recover(self, + f: Callable[[E], Effect[Any, E2, A]]) -> Effect[Any, E2, A]: + """ + Create new :class:`Effect` that applies ``f`` to the error result of \ + running this effect if it fails. If this :class:`Effect` succeeds, \ + ``f`` is not applied. + + :example: + >>> error('Whoops!').recover(lambda _: success('Phew!')).run(None) + 'Phew!' + + :param f: Function to pass the error result of this :class:`Effect` \ + instance once it can be computed + + :return: New :class:`Effect` which wraps the result of \ + passing the error result of this :class:`Effect` instance to ``f`` + """ + async def run_e(r: R) -> Trampoline[Either[E2, A]]: + async def thunk(): + def cont(either: Either): + if isinstance(either, Right): + return Done(either) + + async def thunk(): + next_ = f(either.get) + if asyncio.iscoroutine(next_): + effect = await next_ + else: + effect = next_ + return await effect.run_e(r) + + return Call(thunk) + + trampoline = await self.run_e(r) + return trampoline.and_then(cont) + + return Call(thunk) + + return Effect(run_e) + + def run(self, r: R, asyncio_run=asyncio.run) -> A: + """ + Run the function wrapped by this :class:`Effect`, including potential \ + side-effects. If the function fails the resulting error will be \ + raised as an exception. + + :param r: The environment with which to run this :class:`Effect` + :param asyncio_run: Function to run the coroutine returned by the \ + wrapped function + :returns: The succesful result of the wrapped functions if it succeeds + """ + async def _run() -> A: + trampoline = await self.run_e(r) # type: ignore + result = await trampoline.run() + if isinstance(result, Left): + error = result.get + if isinstance(error, Exception): + raise error + else: + raise RuntimeError(repr(error)) + else: + return result.get + + return asyncio_run(_run()) + + __call__ = run + + def map(self, f: Callable[[A], Union[Awaitable[B], B]]) -> Effect[R, E, B]: + """ + Map `f` over the produced by this :class:`Effect` once it is run + + :example: + >>> success(2).map(lambda v: v + 2).run(None) + 4 + + :param f: function to map over this :class:`Effect` + :return: new :class:`Effect` with `f` applied to the \ + value produced by this :class:`Effect`. + """ + async def run_e(r: R) -> Trampoline[Either[E, B]]: + def cont(either): + async def thunk() -> Trampoline[Either]: + result = f(either.get) + if asyncio.iscoroutine(result): + result = await result # type: ignore + return Done(Right(result)) + + if isinstance(either, Left): + return Done(either) + return Call(thunk) + + trampoline = await self.run_e(r) # type: ignore + return trampoline.and_then(cont) + + return Effect(run_e) + + +R1 = TypeVar('R1') +E1 = TypeVar('E1') +A1 = TypeVar('A1') + + +def success(value: A1) -> Effect[Any, NoReturn, A1]: + """ + Wrap a function in :class:`Effect` that does nothing but return ``value`` + + :example: + >>> success('Yay!').run(None) + 'Yay!' + + :param value: The value to return when the :class:`Effect` is executed + :return: :class:`Effect` that wraps a function returning ``value`` + """ + async def run_e(_): + return Done(Right(value)) + + return Effect(run_e) + + +def get_environment() -> Effect[Any, NoReturn, Any]: + """ + Get an :class:`Effect` that produces the environment passed to `run` \ + when executed + + :example: + >>> get_environment().run('environment') + 'environment' + + :return: :class:`Effect` that produces the enviroment passed to `run` + """ + async def run_e(r): + return Done(Right(r)) + + return Effect(run_e) + + +def from_awaitable(awaitable: Awaitable[A1]) -> Effect[Any, NoReturn, A1]: + """ + Create an :class:`Effect` that produces the result of awaiting `awaitable` + + :example: + >>> async def f() -> str: + ... return 'Yay!' + >>> from_awaitable(f()).run(None) + 'Yay' + + :param awaitable: Awaitable to await in the resulting :class:`Effect` + :return: :class:`Effect` that produces the result of awaiting `awaitable` + """ + async def run_e(_): + return Done(Right(await awaitable)) + + return Effect(run_e) + + +B1 = TypeVar('B1') +Effects = Generator[Effect[R1, E1, Any], Any, B1] + + +def with_effect(f: Callable[..., Effects[R1, E1, A1]] + ) -> Callable[..., Effect[R1, E1, A1]]: + """ + Decorator for functions generating :class:`Effect` instances. Will + chain together the generated effectss using `and_then` + + :example: + >>> @with_effect + ... def f() -> Effects[Any, NoReturn, int]: + ... a = yield success(2) + ... b = yield success(2) + ... return a + b + >>> f().run(None) + 4 + + :param f: the function to decorate + :return: new function that consumes effects generated by `f`, \ + chaining them together with `and_then` + """ + @wraps(f) + def decorator(*args, **kwargs): + g = f(*args, **kwargs) + + def cont(v): + try: + return g.send(v).and_then(cont) + except StopIteration as e: + return success(e.value) + + try: + m = next(g) + return m.and_then(cont) + except StopIteration as e: + return success(e.value) + + return decorator + + +def sequence_async(iterable: Iterable[Effect[R1, E1, A1]] + ) -> Effect[R1, E1, Iterable[A1]]: + """ + Evaluate each :class:`Effect` in `iterable` asynchronously + and collect the results + + :example: + >>> sequence_async([success(v) for v in range(3)]).run(None) + (0, 1, 2) + + :param iterable: The iterable to collect results from + :returns: ``Effect`` that produces collected results + """ + async def run_e(r): + awaitables = [e.run_e(r) for e in iterable] + trampolines = await asyncio.gather(*awaitables) + # TODO should this be run in an executor to avoid blocking? + # maybe depending on the number of effects? + trampoline = sequence_trampolines(trampolines) + return trampoline.map(lambda eithers: sequence_eithers(eithers)) + + return Effect(run_e) + + +@curry +def map_m(f: Callable[[A1], Effect[R1, E1, A1]], + iterable: Iterable[A1]) -> Effect[R1, E1, Iterable[A1]]: + """ + Map each in element in ``iterable`` to + an :class:`Effect` by applying ``f``, + combine the elements by ``and_then`` + from left to right and collect the results + + :example: + >>> map_m(success, range(3)).run(None) + (0, 1, 2) + + :param f: Function to map over ``iterable`` + :param iterable: Iterable to map ``f`` over + :return: ``f`` mapped over ``iterable`` and combined from left to right. + """ + effects = (f(x) for x in iterable) + return sequence_async(effects) + + +@curry +def filter_m(f: Callable[[A], Effect[R1, E1, bool]], + iterable: Iterable[A]) -> Effect[R1, E1, Iterable[A]]: + """ + Map each element in ``iterable`` by applying ``f``, + filter the results by the value returned by ``f`` + and combine from left to right. + + :example: + >>> filter_m(lambda v: success(v % 2 == 0), range(3)).run(None) + (0, 2) + + :param f: Function to map ``iterable`` by + :param iterable: Iterable to map by ``f`` + :return: `iterable` mapped and filtered by `f` + """ + async def run_e(r): + async def thunk(): + awaitables = [f(a).run_e(r) for a in iterable] + trampolines = await asyncio.gather(*awaitables) + trampoline = sequence_trampolines(trampolines) + return trampoline.map( + lambda eithers: sequence_eithers(eithers). + map(lambda bs: tuple(a for a, b in zip(iterable, bs) if b)) + ) + + return Call(thunk) + + return Effect(run_e) + + +def absolve(effect: Effect[Any, NoReturn, Either[E1, A1]] + ) -> Effect[Any, E1, A1]: + """ + Move the error type from an :class:`Effect` producing an :class:`Either` \ + into the error channel of the :class:`Effect` + + :example: + >>> effect = error('Whoops').either().map( + ... lambda either: either.get if isinstance(either, Right) else 'Phew!' + ... ) + >>> absolve(effect).run(None) + 'Phew!' + + :param effect: an :class:`Effect` producing an :class:`Either` + :return: an :class:`Effect` failing with `E1` or succeeding with `A1` + """ + async def run_e(r) -> Trampoline[Either[E1, A1]]: + async def thunk(): + trampoline = await effect.run_e(r) + return trampoline.and_then(lambda either: Done(either.get)) + + return Call(thunk) + + return Effect(run_e) + + +def error(reason: E1) -> Effect[Any, E1, NoReturn]: + """ + Create an :class:`Effect` that does nothing but fail with `reason` + + :example: + >>> error('Whoops!').run(None) + RuntimeError: 'Whoops!' + + :param reason: Value to fail with + :return: :class:`Effect` that fails with `reason` + """ + async def run_e(r): + return Done(Left(reason)) + + return Effect(run_e) + + +A2 = TypeVar('A2') + + +def combine(*effects: Effect[R1, E1, A2] + ) -> Callable[[Callable[..., A1]], Effect[Any, Any, A1]]: + """ + Create an effect that produces the result of calling the passed function \ + with the results of effects in `effects` + + :example: + >>> combine(success(2), success(2))(lambda a, b: a + b).run(None) + 4 + + :param effects: Effects the results of which to pass to the combiner \ + function + + :return: function that takes a combiner function and returns an \ + :class:`Effect` that applies the function to the results of `effects` + """ + def _(f: Callable[..., A1]): + effect = sequence_async(effects) + return effect.map(lambda seq: f(*seq)) + + return _ + + +EX = TypeVar('EX', bound=Exception) + + +# @curry +def catch(error_type: Type[EX], + ) -> Callable[[Callable[[], A1]], Effect[Any, EX, A1]]: + """ + Catch exceptions raised by a function and push them into the error type \ + of an :class:`Effect` + + :example: + >>> catch(ZeroDivisionError)(lambda: 1 / 0).either().run(None) + Left(ZeroDivisionError('division by zero')) + + :param error_type: Exception type to catch. All other exceptions will \ + not be handled + :return: :class:`Effect` that can fail with exceptions raised by the \ + passed function + """ + def _(f): + try: + return success(f()) + except error_type as e: + return error(e) + + return _ + + +def catch_all(f: Callable[[], A1]) -> Effect[Any, Exception, A1]: + """ + Return an :class:`Effect` that can fail with any exceptions raised by `f` + + :example: + >>> catch_all(lambda: 1 / 0).either().run(None) + Left(ZeroDivisionError('division by zero')) + + :param f: All exceptions raised by this functions will be pushed to the \ + error channel of the resulting :class:`Effect` + :return: :class:`Effect` that cain fail with exceptions raised by `f` \ + or succeed with the result of `f` + """ + async def run_e(_: Any) -> Trampoline[Either[Exception, A1]]: + try: + return Done(Right(f())) + except Exception as e: + return Done(Left(e)) + + return Effect(run_e) + + +__all__ = [ + 'Effect', + 'success', + 'get_environment', + 'with_effect', + 'sequence_async', + 'filter_m', + 'map_m', + 'absolve', + 'error', + 'combine', + 'catch', + 'catch_all', + 'from_awaitable' +] diff --git a/pfun/effect/files.py b/pfun/effect/files.py new file mode 100644 index 0000000..3b71360 --- /dev/null +++ b/pfun/effect/files.py @@ -0,0 +1,287 @@ +from typing import Any + +from typing_extensions import Protocol + +from ..aio_trampoline import Done, Trampoline +from ..curry import curry +from ..either import Either, Left, Right +from ..immutable import Immutable +from .effect import Effect, get_environment + + +class Files(Immutable): + """ + Module that enables reading and writing from files + """ + def read(self, path: str) -> Effect[Any, OSError, str]: + """ + get an :class:`Effect` that reads the content of a file as a str + + :example: + >>> Files().read('foo.txt').run(None) + 'contents of foo.txt' + + :param path: path to file + :return: :class:`Effect` that reads file located at `path` + """ + async def run_e(_) -> Trampoline[Either[OSError, str]]: + try: + with open(path) as f: + contents = f.read() + return Done(Right(contents)) + except OSError as e: + return Done(Left(e)) + + return Effect(run_e) + + def read_bytes(self, path: str) -> Effect[Any, OSError, bytes]: + """ + get an :class:`Effect` that reads the content of a file as bytes + + :example: + >>> Files().read_bytes('foo.txt').run(None) + b'contents of foo.txt' + + :param path: path to file + :return: :class:`Effect` that reads file located at `path` + """ + async def run_e(_) -> Trampoline[Either[OSError, bytes]]: + try: + with open(path, 'b') as f: + contents = f.read() + return Done(Right(contents)) + except OSError as e: + return Done(Left(e)) + + return Effect(run_e) + + def write(self, path: str, content: str) -> Effect[Any, OSError, None]: + """ + Get an :class:`Effect` that writes to a file + + :example: + >>> files = Files() + >>> files\\ + ... .write('foo.txt', 'contents')\\ + ... .discard_and_then(files.read('foo.txt'))\\ + ... .run(None) + 'contents' + + :param path: the path of the file to be written + :param: content the content to write + :return: :class:`Effect` that that writes `content` to file at `path` + """ + async def run_e(_) -> Trampoline[Either[OSError, None]]: + try: + with open(path, 'w') as f: + f.write(content) + return Done(Right(None)) + except OSError as e: + return Done(Left(e)) + + return Effect(run_e) + + def write_bytes(self, path: str, + content: bytes) -> Effect[Any, OSError, None]: + """ + Get an :class:`Effect` that writes to a file + + :example: + >>> files = Files() + >>> files\\ + ... .write_bytes('foo.txt', b'contents')\\ + ... .discard_and_then(files.read('foo.txt'))\\ + ... .run(None) + 'contents' + + :param path: the path of the file to be written + :param: content the content to write + :return: :class:`Effect` that that writes `content` to file at `path` + """ + async def run_e(_) -> Trampoline[Either[OSError, None]]: + try: + with open(path, 'wb') as f: + f.write(content) + return Done(Right(None)) + except OSError as e: + return Done(Left(e)) + + return Effect(run_e) + + def append(self, path: str, content: str) -> Effect[Any, OSError, None]: + """ + Get an :class:`Effect` that appends to a file + + :example: + >>> files = Files() + >>> files\\ + ... .append('foo.txt', 'contents')\\ + ... .discard_and_then(files.read('foo.txt'))\\ + ... .run(None) + 'contents' + + :param path: the path of the file to be written + :param: content the content to append + :return: :class:`Effect` that that appends `content` to file at `path` + """ + async def run_e(_) -> Trampoline[Either[OSError, None]]: + try: + with open(path, 'a+') as f: + f.write(content) + return Done(Right(None)) + except OSError as e: + return Done(Left(e)) + + return Effect(run_e) + + def append_bytes(self, path: str, + content: bytes) -> Effect[Any, OSError, None]: + """ + Get an :class:`Effect` that appends to a file + + :example: + >>> files = Files() + >>> files\\ + ... .append_bytes('foo.txt', b'contents')\\ + ... .discard_and_then(files.read('foo.txt'))\\ + ... .run(None) + 'contents + + :param path: the path of the file to be written + :param: content the content to append + :return: :class:`Effect` that that appends `content` to file at `path` + """ + async def run_e(_) -> Trampoline[Either[OSError, None]]: + try: + with open(path, 'ab+') as f: + f.write(content) + return Done(Right(None)) + except OSError as e: + return Done(Left(e)) + + return Effect(run_e) + + +class HasFiles(Protocol): + """ + Module provider that provides the files module + + :attribute files: The :class:`Files` instance + """ + files: Files + + +def read(path: str) -> Effect[HasFiles, OSError, str]: + """ + get an :class:`Effect` that reads the content of a file as a str + + :example: + >>> class Env: + ... files = Files() + >>> read('foo.txt').run(Env()) + 'contents of foo.txt' + + :param path: path to file + :return: :class:`Effect` that reads file located at `path` + """ + return get_environment().and_then(lambda env: env.files.read(path)) + + +@curry +def write(path: str, content: str) -> Effect[HasFiles, OSError, None]: + """ + Get an :class:`Effect` that writes to a file + + :example: + >>> class Env: + ... files = Files() + >>> write('foo.txt')('contents')\\ + ... .discard_and_then(read('foo.txt'))\\ + ... .run(Env()) + 'content of foo.txt' + + :param path: the path of the file to be written + :param: content the content to write + :return: :class:`Effect` that that writes `content` to file at `path` + """ + return get_environment( + ).and_then(lambda env: env.files.write(path, content)) + + +def read_bytes(path: str) -> Effect[HasFiles, OSError, bytes]: + """ + get an :class:`Effect` that reads the content of a file as bytes + + :example: + >>> class Env: + ... files = Files() + >>> read_bytes('foo.txt').run(Env()) + b'contents of foo.txt' + + :param path: path to file + :return: :class:`Effect` that reads file located at `path` + """ + return get_environment().and_then(lambda env: env.files.read_bytes(path)) + + +@curry +def write_bytes(path: str, content: bytes) -> Effect[HasFiles, OSError, None]: + """ + Get an :class:`Effect` that writes to a file + + :example: + >>> class Env: + ... files = Files() + >>> write_bytes('foo.txt')(b'content of foo.txt')\\ + ... .discard_and_then(read('foo.txt'))\\ + ... .run(Env()) + 'content of foo.txt' + + :param path: the path of the file to be written + :param: content the content to write + :return: :class:`Effect` that that writes `content` to file at `path` + """ + return get_environment( + ).and_then(lambda env: env.files.write_bytes(path, content)) + + +@curry +def append(path: str, content: str) -> Effect[HasFiles, OSError, None]: + """ + Get an :class:`Effect` that appends to a file + + :example: + >>> class Env: + ... files = Files() + >>> append('foo.txt')('content of foo.txt')\\ + ... .discard_and_then(read('foo.txt'))\\ + ... .run(Env()) + 'content of foo.txt' + + :param path: the path of the file to be written + :param: content the content to append + :return: :class:`Effect` that that appends `content` to file at `path` + """ + return get_environment( + ).and_then(lambda env: env.files.append(path, content)) + + +@curry +def append_bytes(path: str, content: bytes) -> Effect[HasFiles, OSError, None]: + """ + Get an :class:`Effect` that appends to a file + + :example: + >>> class Env: + ... files = Files() + >>> append_bytes('foo.txt')(b'content of foo.txt')\\ + ... .discard_and_then(read('foo.txt'))\\ + ... .run(Env()) + 'content of foo.txt' + + :param path: the path of the file to be written + :param: content the content to append + :return: :class:`Effect` that that appends `content` to file at `path` + """ + return get_environment( + ).and_then(lambda env: env.files.append_bytes(path, content)) diff --git a/pfun/effect/ref.py b/pfun/effect/ref.py new file mode 100644 index 0000000..77f884c --- /dev/null +++ b/pfun/effect/ref.py @@ -0,0 +1,137 @@ +from asyncio import Lock +from typing import Any, Callable, Generic, NoReturn, Optional, TypeVar, cast + +from ..aio_trampoline import Done, Trampoline +from ..either import Either, Left, Right +from ..immutable import Immutable +from .effect import Effect + +A = TypeVar('A') +E = TypeVar('E') + + +class Ref(Immutable, Generic[A]): + """ + Wraps a value that can be mutated as an :class:`Effect` + + :attribute value: the wrapped value + :attribute lock: locks mutation of `value` + """ + value: A + lock: Optional[Lock] = None + + @property + def __lock(self) -> Lock: + # All this nonsense is to ensure that locks are not initialised + # before the thread running the event loop is initialised. + # If the lock is initialised in the main thread, + # it may lead to + # RuntimeError: There is no current event loop in thread 'MainThread'. + # see https://tinyurl.com/yc9kd77s + # In theory, users could still get this wrong by supplying + # their own lock as Ref(value, Lock()), + # but then they are on their own ¯\_(ツ)_/¯ + if self.lock is None: + object.__setattr__(self, 'lock', Lock()) + return cast(Lock, self.lock) + + def get(self) -> Effect[Any, NoReturn, A]: + """ + Get an :class:`Effect` that reads the current state of the value + + :example: + >>> ref = Ref('the state') + >>> ref.get().run(None) + 'the state' + + :return: :class:`Effect` that reads the current state + """ + async def run_e(_) -> Trampoline[Either[NoReturn, A]]: + async with self.__lock: + return Done(Right(self.value)) + + return Effect(run_e) + + def __repr__(self): + return f'Ref({repr(self.value)})' + + def put(self, value: A) -> Effect[Any, NoReturn, None]: + """ + Get an :class:`Effect` that updates the current state of the value + + :example: + >>> ref = Ref('initial state') + >>> ref.put('new state').run(None) + None + >>> ref.value + 'new state' + + :param value: new state + :return: :class:`Effect` that updates the state + """ + async def run_e(_) -> Trampoline[Either[NoReturn, None]]: + async with self.__lock: + # purists avert your eyes + object.__setattr__(self, 'value', value) + return Done(Right(None)) + + return Effect(run_e) + + def modify(self, f: Callable[[A], A]) -> Effect[Any, NoReturn, None]: + """ + Modify the value wrapped by this :class:`Ref` by \ + applying `f` in isolation + + :example: + >>> ref = Ref([]) + >>> ref.modify(lambda l: l + [1]).run(None) + None + >>> ref.value + [1] + + :param f: function that accepts the current state and returns \ + a new state + :return: :class:`Effect` that updates the state to the result of `f` + """ + async def run_e(_) -> Trampoline[Either[NoReturn, None]]: + async with self.__lock: + new = f(self.value) + object.__setattr__(self, 'value', new) + return Done(Right(None)) + + return Effect(run_e) + + def try_modify(self, + f: Callable[[A], Either[E, A]]) -> Effect[Any, E, None]: + """ + Try to update the current state with the result of `f` if it succeeds. + The state is updated if `f` returns a :class:`Right` value, and kept + as is otherwise + + :example: + >>> from pfun.either import Left, Right + >>> ref = Ref('initial state') + >>> ref.try_modify(lambda _: Left('Whoops!')).run(None) + None + >>> ref.value + 'initial state' + >>> ref.try_modify(lambda _: Right('new state')).run(None) + None + >>> ref.value + 'new state' + + :param f: function that accepts the current state and \ + returns a :class:`Right` wrapping a new state \ + or a :class:`Left` value wrapping an error + :return: an :class:`Effect` that updates the state if `f` succeeds + """ + async def run_e(_) -> Trampoline[Either[E, None]]: + async with self.__lock: + either = f(self.value) + if isinstance(either, Left): + return Done(either) + else: + object.__setattr__(self, 'value', either.get) + return Done(Right(None)) + + return Effect(run_e) diff --git a/pfun/effect/subprocess.py b/pfun/effect/subprocess.py new file mode 100644 index 0000000..8406918 --- /dev/null +++ b/pfun/effect/subprocess.py @@ -0,0 +1,89 @@ +import asyncio +from subprocess import PIPE, CalledProcessError +from typing import IO, Any, Tuple, Union + +from typing_extensions import Protocol + +from ..aio_trampoline import Done +from ..either import Left, Right +from ..immutable import Immutable +from .effect import Effect, get_environment + + +class Subprocess(Immutable): + """ + Module that enables running commands in the shell + """ + def run_in_shell( + self, + cmd: str, + stdin: Union[IO, int] = PIPE, + stdout: Union[IO, int] = PIPE, + stderr: Union[IO, int] = PIPE + ) -> Effect[Any, CalledProcessError, Tuple[bytes, bytes]]: + """ + Get an :class:`Effect` that runs `cmd` in the shell + + :example: + >>> Subprocess().run_in_shell('cat foo.txt').run(None) + (b'contents of foo.txt', b'') + + :param cmd: the command to run + :param stdin: input pipe for the subprocess + :param stdout: output pipe for the subprocess + :param stderr: error pipe for the subprocess + :return: :class:`Effect` that runs `cmd` in the shell and produces \ + a tuple of `(stdout, stderr)` + """ + async def run_e(self): + proc = await asyncio.create_subprocess_shell( + cmd, stdin=stdin, stdout=stdout, stderr=stderr + ) + stdout_, stderr_ = await proc.communicate() + if proc.returncode != 0: + return Done( + Left( + CalledProcessError( + proc.returncode, cmd, stdout_, stderr_ + ) + ) + ) + return Done(Right((stdout_, stderr_))) + + return Effect(run_e) + + +class HasSubprocess(Protocol): + """ + Module provider providing the subprocess module + + :attribute subprocess: the :class:`Subprocess` instance + """ + subprocess: Subprocess + + +def run_in_shell( + cmd: str, + stdin: Union[IO, int] = PIPE, + stdout: Union[IO, int] = PIPE, + stderr: Union[IO, int] = PIPE +) -> Effect[HasSubprocess, CalledProcessError, Tuple[bytes, bytes]]: + """ + Get an :class:`Effect` that runs `cmd` in the shell + + :example: + >>> class Env: + ... subprocess = Subprocess() + >>> run_in_shell('cat foo.txt').run(Env()) + (b'contents of foo.txt', b'') + + :param cmd: the command to run + :param stdin: input pipe for the subprocess + :param stdout: output pipe for the subprocess + :param stderr: error pipe for the subprocess + :return: :class:`Effect` that runs `cmd` in the shell and produces \ + a tuple of `(stdout, stderr)` + """ + return get_environment().and_then( + lambda env: env.subprocess.run_in_shell(cmd, stdin, stdout, stderr) + ) diff --git a/pfun/either.py b/pfun/either.py index 67eeaab..3e6907e 100644 --- a/pfun/either.py +++ b/pfun/either.py @@ -1,22 +1,22 @@ from __future__ import annotations -from typing import ( - Generic, TypeVar, Callable, Any, Iterable, cast, Union, Generator -) -from functools import wraps from abc import ABC, abstractmethod +from functools import wraps +from typing import (Any, Callable, Generator, Generic, Iterable, TypeVar, + Union, cast) -from .immutable import Immutable -from .monad import Monad, sequence_, map_m_, filter_m_ from .curry import curry +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ from .with_effect import with_effect_tail_rec -A = TypeVar('A') -B = TypeVar('B') +A = TypeVar('A', covariant=True) +B = TypeVar('B', covariant=True) C = TypeVar('C') +D = TypeVar('D') -class Either_(Immutable, Monad, ABC): # type: ignore +class Either_(Immutable, Monad, ABC): """ Abstract class representing a computation with either ``A`` or ``B`` as its result. @@ -97,16 +97,16 @@ def map(self, f): raise NotImplementedError() -class Right(Either_, Generic[A]): # type: ignore +class Right(Either_, Generic[A]): """ Represents the ``Right`` case of ``Either`` """ get: A - def or_else(self, default: A) -> A: + def or_else(self, default: C) -> A: return self.get - def map(self, f: Callable[[A], C]) -> Either[B, C]: + def map(self, f: Callable[[A], C]) -> Either[Any, C]: return Right(f(self.get)) def and_then(self, f: Callable[[A], Either[B, C]]) -> Either[B, C]: @@ -137,13 +137,13 @@ def __repr__(self): return f'Right({repr(self.get)})' -class Left(Either_, Generic[B]): # type: ignore +class Left(Either_, Generic[B]): """ Represents the ``Left`` case of ``Either`` """ get: B - def or_else(self, default: A) -> A: + def or_else(self, default: C) -> C: return default def map(self, f: Callable[[A], C]) -> Either[B, C]: @@ -252,7 +252,7 @@ def filter_m(f: Callable[[A], Either[bool, B]], return cast(Either[Iterable[A], B], filter_m_(Right, f, iterable)) -def tail_rec(f: Callable[[A], Either[C, Either[A, B]]], a: A) -> Either[C, B]: +def tail_rec(f: Callable[[D], Either[C, Either[D, B]]], a: D) -> Either[C, B]: """ Run a stack safe recursive monadic function `f` by calling `f` with :class:`Left` values @@ -309,6 +309,17 @@ def with_effect(f: Callable[..., Eithers[A, B, C]] return with_effect_tail_rec(Right, f, tail_rec) # type: ignore +def catch(f: Callable[..., A]) -> Callable[..., Either[Exception, A]]: + @wraps(f) + def decorator(*args, **kwargs) -> Either[Exception, A]: + try: + return Right(f(*args, **kwargs)) + except Exception as e: + return Left(e) + + return decorator + + __all__ = [ 'Either', 'Left', @@ -318,5 +329,6 @@ def with_effect(f: Callable[..., Eithers[A, B, C]] 'sequence', 'filter_m', 'with_effect', - 'Eithers' + 'Eithers', + 'catch' ] diff --git a/pfun/free.py b/pfun/free.py index 3faf4e6..62df146 100644 --- a/pfun/free.py +++ b/pfun/free.py @@ -1,11 +1,11 @@ -from typing import TypeVar, Generic, Callable, Iterable, cast, Generator +from abc import ABC, abstractmethod +from typing import Callable, Generator, Generic, Iterable, TypeVar, cast from pfun.immutable import Immutable -from abc import ABC, abstractmethod -from .functor import Functor -from .monad import Monad, sequence_, map_m_, filter_m_ from .curry import curry +from .functor import Functor +from .monad import Monad, filter_m_, map_m_, sequence_ from .state import State, get from .with_effect import with_effect_ @@ -54,7 +54,7 @@ def accept(self, interpreter: FreeInterpreter[C, D]) -> State[C, D]: F = TypeVar('F', bound=Functor) -class Free( # type: ignore +class Free( Generic[F, A, C, D], FreeInterpreterElement[C, D], Monad, Immutable ): """ @@ -70,7 +70,7 @@ def map(self, f: Callable[[A], B]) -> 'Free[F, B, C, D]': return self.and_then(lambda v: Done(f(v))) -class Done(Free[F, A, C, D]): # type: ignore +class Done(Free[F, A, C, D]): """ Pure ``Free`` value """ @@ -95,7 +95,7 @@ def accept(self, interpreter: FreeInterpreter[C, D]) -> State[C, D]: return interpreter.interpret_done(self) -class More(Free[F, A, C, D]): # type: ignore +class More(Free[F, A, C, D]): """ A ``Free`` value wrapping a `Functor` value """ diff --git a/pfun/functor.py b/pfun/functor.py index 2ac9434..54930b8 100644 --- a/pfun/functor.py +++ b/pfun/functor.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Callable, Any +from typing import Any, Callable class Functor(ABC): diff --git a/pfun/immutable.py b/pfun/immutable.py index cce3778..bcef1be 100644 --- a/pfun/immutable.py +++ b/pfun/immutable.py @@ -6,10 +6,10 @@ @dataclass(frozen=True) class Immutable: - __immutable__ = True """ Super class that makes subclasses immutable using dataclasses + :example: >>> class A(Immutable): ... a: str >>> class B(A): @@ -19,6 +19,8 @@ class Immutable: AttributeError: <__main__.B object at 0x10f99a0f0> is immutable """ + __immutable__ = True + def __init_subclass__( cls, init=True, repr=True, eq=True, order=False, unsafe_hash=False ): diff --git a/pfun/io.py b/pfun/io.py index e8b5be2..6d280aa 100644 --- a/pfun/io.py +++ b/pfun/io.py @@ -1,21 +1,22 @@ from __future__ import annotations -from typing import Callable, TypeVar, Generic, Generator, Iterable, cast + import sys from functools import wraps +from typing import Callable, Generator, Generic, Iterable, TypeVar, cast from typing_extensions import Literal -from .immutable import Immutable -from .trampoline import Trampoline, Done, Call -from .monad import Monad, sequence_, map_m_, filter_m_ from .curry import curry +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ +from .trampoline import Call, Done, Trampoline from .with_effect import with_effect_ A = TypeVar('A') B = TypeVar('B') -class IO(Monad, Immutable, Generic[A]): # type: ignore +class IO(Monad, Immutable, Generic[A]): """ Represents world changing actions """ @@ -203,8 +204,8 @@ def map_m(f: Callable[[A], IO[B]], iterable: Iterable[A]) -> IO[Iterable[B]]: from left to right and collect the results :example: - >>> map_m(IO, range(3)) - IO(a=(0, 1, 2)) + >>> map_m(IO, range(3)).run() + (0, 1, 2) :param f: Function to map over ``iterable`` :param iterable: Iterable to map ``f`` over @@ -219,11 +220,11 @@ def sequence(iterable: Iterable[IO[A]]) -> IO[Iterable[A]]: and collect the results :example: - >>> sequence([IO(v) for v in range(3)]) - Just(a=(0, 1, 2)) + >>> sequence([IO(v) for v in range(3)]).run() + (0, 1, 2) :param iterable: The iterable to collect results from - :returns: ``Maybe`` of collected results + :returns: ``IO`` of collected results """ return cast(IO[Iterable[A]], sequence_(value, iterable)) @@ -237,8 +238,8 @@ def filter_m(f: Callable[[A], IO[bool]], and combine from left to right. :example: - >>> filter_m(lambda v: IO(v % 2 == 0), range(3)) - IO(a=(0, 2)) + >>> filter_m(lambda v: IO(v % 2 == 0), range(3)).run() + (0, 2) :param f: Function to map ``iterable`` by :param iterable: Iterable to map by ``f`` diff --git a/pfun/list.py b/pfun/list.py index ee87033..640c9e2 100644 --- a/pfun/list.py +++ b/pfun/list.py @@ -1,23 +1,18 @@ -from typing import ( - TypeVar, Callable, Iterable, Tuple, Optional, Generic, cast, Generator -) from functools import reduce +from typing import (Callable, Generator, Generic, Iterable, Optional, Tuple, + TypeVar, cast) -from .monoid import Monoid -from .immutable import Immutable from .curry import curry -from .monad import map_m_, sequence_, filter_m_, Monad +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ +from .monoid import Monoid from .with_effect import with_effect_eager + A = TypeVar('A') B = TypeVar('B') -class List(Monoid, # type: ignore - Monad, - Generic[A], - Iterable[A], - Immutable, - init=False): +class List(Monoid, Monad, Generic[A], Iterable[A], Immutable, init=False): _iterable: Tuple[A] def __init__(self, iterable: Iterable[A] = ()): @@ -45,7 +40,7 @@ def reduce( """ return reduce(f, self._iterable, initializer) # type: ignore - def append(self, a: Iterable[A]) -> 'List[A]': + def append(self, a: A) -> 'List[A]': """ Add element to end of list @@ -56,7 +51,7 @@ def append(self, a: Iterable[A]) -> 'List[A]': :param a: Element to append :return: New :class:`List` with ``a`` appended """ - return List(self._iterable + tuple(a)) + return List(self._iterable + (a,)) def extend(self, iterable: Iterable[A]) -> 'List[A]': """ diff --git a/pfun/maybe.py b/pfun/maybe.py index 13575cf..8ee027d 100644 --- a/pfun/maybe.py +++ b/pfun/maybe.py @@ -1,30 +1,20 @@ -from typing import ( - Generic, - TypeVar, - Callable, - Any, - Sequence, - Iterable, - cast, - Generator, - Union, - Optional -) -from functools import wraps from abc import ABC, abstractmethod +from functools import wraps +from typing import (Any, Callable, Generator, Generic, Iterable, Optional, + Sequence, TypeVar, Union, cast) +from .curry import curry +from .either import Either, Left from .immutable import Immutable from .list import List -from .curry import curry -from .monad import Monad, map_m_, sequence_, filter_m_ +from .monad import Monad, filter_m_, map_m_, sequence_ from .with_effect import with_effect_tail_rec -from .either import Either, Left A = TypeVar('A') B = TypeVar('B') -class Maybe_(Immutable, Monad, ABC): # type: ignore +class Maybe_(Immutable, Monad, ABC): """ Abstract super class for classes that represent computations that can fail. Should not be instantiated directly. @@ -119,7 +109,7 @@ def _invoke_optional_arg( raise -class Just(Maybe_, Generic[A]): # type: ignore +class Just(Maybe_, Generic[A]): """ Subclass of :class:`Maybe` that represents a successful computation @@ -155,7 +145,7 @@ def __bool__(self): return True -class Nothing(Maybe_): # type: ignore +class Nothing(Maybe_): """ Subclass of :class:`Maybe` that represents a failed computation diff --git a/pfun/monad.py b/pfun/monad.py index d40beb1..163d88f 100644 --- a/pfun/monad.py +++ b/pfun/monad.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod -from typing import Callable, Any, Iterable from functools import reduce +from typing import Any, Callable, Iterable -from .functor import Functor from .curry import curry +from .functor import Functor class Monad(Functor, ABC): diff --git a/pfun/monoid.py b/pfun/monoid.py index c588899..7138fec 100644 --- a/pfun/monoid.py +++ b/pfun/monoid.py @@ -1,7 +1,6 @@ -from functools import singledispatch - -from typing import Union, List, Tuple, TypeVar from abc import ABC, abstractmethod +from functools import singledispatch +from typing import List, Tuple, TypeVar, Union class Monoid(ABC): @@ -11,7 +10,7 @@ class Monoid(ABC): """ @abstractmethod - def append(self, other): + def __add__(self, other): """ Append function for the Monoid type @@ -41,7 +40,7 @@ def append(a: M, b: M) -> M: @append.register def append_monoid(a: Monoid, b: Monoid) -> Monoid: - return a.append(b) + return a + b @append.register diff --git a/pfun/mypy_plugin.py b/pfun/mypy_plugin.py index 1c81c2d..95392b7 100644 --- a/pfun/mypy_plugin.py +++ b/pfun/mypy_plugin.py @@ -1,22 +1,17 @@ # type: ignore import typing as t +from functools import reduce -from mypy.plugin import Plugin, FunctionContext, ClassDefContext -from mypy.plugins.dataclasses import DataclassTransformer -from mypy.types import ( - Type, - CallableType, - Instance, - TypeVarType, - Overloaded, - TypeVarId, - TypeVarDef, - AnyType -) -from mypy.nodes import ClassDef, ARG_POS from mypy import checkmember, infer from mypy.checker import TypeChecker +from mypy.mro import calculate_mro +from mypy.nodes import ARG_POS, Block, ClassDef, NameExpr, TypeInfo +from mypy.plugin import ClassDefContext, FunctionContext, MethodContext, Plugin +from mypy.plugins.dataclasses import DataclassTransformer +from mypy.types import (ARG_POS, AnyType, CallableType, Instance, Overloaded, + Type, TypeVarDef, TypeVarId, TypeVarType, UnionType, + get_proper_type) _CURRY = 'pfun.curry.curry' _COMPOSE = 'pfun.util.compose' @@ -36,6 +31,8 @@ _READER = 'pfun.reader.reader' _EITHER = 'pfun.either.either' _READER_AND_THEN = 'pfun.reader.Reader.and_then' +_EFFECT_COMBINE = 'pfun.effect.combine' +_EITHER_CATCH = 'pfun.either.catch' def _get_callable_type(type_: Type, @@ -209,8 +206,196 @@ def _immutable_hook(context: ClassDefContext): transformer._freeze(attributes) -def _do_hook(context: FunctionContext) -> Type: - return AnyType(6) +def _combine_protocols(p1: Instance, p2: Instance) -> Instance: + def base_repr(base): + if 'pfun.effect.Intersection' in base.type.fullname: + return ', '.join([repr(b) for b in base.type.bases]) + return repr(base) + + def get_bases(base): + if 'pfun.effect.Intersection' in base.type.fullname: + bases = set() + for b in base.type.bases: + bases |= get_bases(b) + return bases + return set([base]) + + names = p1.type.names.copy() + names.update(p2.type.names) + keywords = p1.type.defn.keywords.copy() + keywords.update(p2.type.defn.keywords) + bases = get_bases(p1) | get_bases(p2) + bases_repr = ', '.join(sorted([repr(base) for base in bases])) + name = f'Intersection[{bases_repr}]' + defn = ClassDef( + name, + Block([]), + p1.type.defn.type_vars + p2.type.defn.type_vars, + [NameExpr(p1.type.fullname), NameExpr(p2.type.fullname)], + None, + list(keywords.items()) + ) + defn.fullname = f'pfun.effect.{name}' + info = TypeInfo(names, defn, '') + info.is_protocol = True + info.is_abstract = True + info.bases = [p1, p2] + info.abstract_attributes = (p1.type.abstract_attributes + + p2.type.abstract_attributes) + calculate_mro(info) + return Instance(info, p1.args + p2.args) + + +def _effect_and_then_hook(context: MethodContext) -> Type: + return_type = context.default_return_type + return_type_args = return_type.args + return_type = return_type.copy_modified(args=return_type_args) + try: + e1 = context.type + r1 = e1.args[0] + e2 = context.arg_types[0][0].ret_type + r2 = e2.args[0] + if r1 == r2: + r3 = r1.copy_modified() + return_type_args[0] = r3 + return return_type.copy_modified(args=return_type_args) + elif isinstance(r1, AnyType): + return_type_args[0] = r2.copy_modified() + return return_type.copy_modified(args=return_type_args) + elif isinstance(r2, AnyType): + return_type_args[0] = r1.copy_modified() + return return_type.copy_modified(args=return_type_args) + elif r1.type.is_protocol and r2.type.is_protocol: + intersection = _combine_protocols(r1, r2) + return_type_args[0] = intersection + return return_type.copy_modified(args=return_type_args) + else: + return return_type + except (AttributeError, IndexError): + return return_type + + +def _get_environment_hook(context: FunctionContext): + if context.api.return_types == []: + return context.default_return_type + type_context = context.api.return_types[-1] + if type_context.type.fullname == 'pfun.effect.effect.Effect': + type_context = get_proper_type(type_context) + args = context.default_return_type.args + inferred_r = type_context.args[0] + args[0] = inferred_r + args[-1] = inferred_r + return context.default_return_type.copy_modified(args=args) + return context.default_return_type + + +def _combine_hook(context: FunctionContext): + result_types = [] + error_types = [] + env_types = [] + try: + for effect_type in context.arg_types[0]: + env_type, error_type, result_type = effect_type.args + env_types.append(env_type) + error_types.append(error_type) + result_types.append(result_type) + map_return_type_def = _type_var_def( + 'R1', 'pfun.effect', context.api.named_type('builtins.object') + ) + map_return_type = TypeVarType(map_return_type_def) + map_function_type = CallableType( + arg_types=result_types, + arg_kinds=[ARG_POS for _ in result_types], + arg_names=[None for _ in result_types], + ret_type=map_return_type, + variables=[map_return_type_def], + fallback=context.api.named_type('builtins.function') + ) + ret_type = context.default_return_type.ret_type + combined_error_type = UnionType(set(error_types)) + ret_type_args = ret_type.args + ret_type_args[1] = combined_error_type + ret_type_args[2] = map_return_type + env_types = [ + env_type for env_type in env_types + if not isinstance(env_type, AnyType) + ] + if len(set(env_types)) == 1: + combined_env_type = env_types[0] + elif env_types and all( + hasattr(env_type, 'type') and env_type.type.is_protocol + for env_type in env_types + ): + combined_env_type = reduce(_combine_protocols, env_types) + else: + combined_env_type = ret_type_args[0] + ret_type_args[0] = combined_env_type + ret_type = ret_type.copy_modified(args=ret_type_args) + return CallableType( + arg_types=[map_function_type], + arg_kinds=[ARG_POS], + arg_names=[None], + variables=[map_return_type_def], + ret_type=ret_type, + fallback=context.api.named_type('builtins.function') + ) + except AttributeError: + return context.default_return_type + + +def _effect_recover_hook(context: MethodContext) -> Type: + return_type = context.default_return_type + return_type_args = return_type.args + return_type = return_type.copy_modified(args=return_type_args) + try: + e1 = context.type + r1 = e1.args[0] + e2 = context.arg_types[0][0].ret_type + r2 = e2.args[0] + if r1 == r2: + r3 = r1.copy_modified() + return_type_args[0] = r3 + return return_type.copy_modified(args=return_type_args) + elif isinstance(r1, AnyType): + return_type_args[0] = r2.copy_modified() + return return_type.copy_modified(args=return_type_args) + elif isinstance(r2, AnyType): + return_type_args[0] = r1.copy_modified() + return return_type.copy_modified(args=return_type_args) + elif r1.type.is_protocol and r2.type.is_protocol: + intersection = _combine_protocols(r1, r2) + return_type_args[0] = intersection + return return_type.copy_modified(args=return_type_args) + else: + return return_type + except AttributeError: + return return_type + + +def _lift_hook(context: FunctionContext) -> Type: + lifted_arg_types = context.arg_types[0][0].arg_types + lifted_ret_type = context.arg_types[0][0].ret_type + return context.default_return_type.copy_modified( + args=lifted_arg_types + [lifted_ret_type] + ) + + +def _lift_call_hook(context: MethodContext) -> Type: + import ipdb + ipdb.set_trace() + arg_types = [] + for arg_type in context.arg_types[0]: + arg_types.append(arg_type.args[-1]) + args = context.type.args[:-1] + ret_type = context.type.args[-1] + function_type = CallableType( + arg_types=args, + arg_kinds=[ARG_POS] * len(args), + arg_names=[None] * len(args), + ret_type=ret_type, + fallback=context.api.named_type('builtins.function') + ) + context.api.expr_checker.check_call(callee=function_type, ) class PFun(Plugin): @@ -234,11 +419,22 @@ def get_function_hook(self, fullname: str _STATE_WITH_EFFECT, _IO_WITH_EFFECT, _TRAMPOLINE_WITH_EFFECT, - _FREE_WITH_EFFECT + _FREE_WITH_EFFECT, + _EITHER_CATCH ): return _variadic_decorator_hook + if fullname == 'pfun.effect.effect.get_environment': + return _get_environment_hook + if fullname == 'pfun.effect.effect.combine': + return _combine_hook return None + def get_method_hook(self, fullname: str): + if fullname == 'pfun.effect.effect.Effect.and_then': + return _effect_and_then_hook + if fullname == 'pfun.effect.effect.Effect.recover': + return _effect_recover_hook + def get_base_class_hook(self, fullname: str): return _immutable_hook diff --git a/pfun/reader.py b/pfun/reader.py index c713d4c..2f1d83c 100644 --- a/pfun/reader.py +++ b/pfun/reader.py @@ -1,10 +1,10 @@ from functools import wraps -from typing import Generic, TypeVar, Callable, Iterable, cast, Generator, Any +from typing import Any, Callable, Generator, Generic, Iterable, TypeVar, cast -from .immutable import Immutable from .curry import curry -from .trampoline import Trampoline, Done, Call -from .monad import Monad, map_m_, sequence_, filter_m_ +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ +from .trampoline import Call, Done, Trampoline from .with_effect import with_effect_ Context = TypeVar('Context') @@ -15,7 +15,7 @@ B = TypeVar('B') -class Reader(Immutable, Generic[Context, Result_], Monad): # type: ignore +class Reader(Immutable, Generic[Context, Result_], Monad): """ Represents a computation that is not yet completed, but will complete once given an object of type ``Context`` diff --git a/pfun/result.py b/pfun/result.py index 4601d06..74b4603 100644 --- a/pfun/result.py +++ b/pfun/result.py @@ -1,17 +1,17 @@ -from typing import TypeVar, Callable, Union from functools import wraps +from typing import Callable, TypeVar, Union -from .either import Left, Right, with_effect, Eithers +from .either import Eithers, Left, Right, with_effect A = TypeVar('A') B = TypeVar('B') -class Ok(Right[A]): # type: ignore +class Ok(Right[A]): pass -class Error(Left[Exception]): # type: ignore +class Error(Left[Exception]): get: Exception diff --git a/pfun/state.py b/pfun/state.py index 3940aff..6768cb1 100644 --- a/pfun/state.py +++ b/pfun/state.py @@ -1,9 +1,9 @@ -from typing import Generic, TypeVar, Callable, Tuple, Iterable, cast, Generator +from typing import Callable, Generator, Generic, Iterable, Tuple, TypeVar, cast -from .immutable import Immutable -from .monad import sequence_, map_m_, filter_m_, Monad from .curry import curry -from .trampoline import Trampoline, Done, Call +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ +from .trampoline import Call, Done, Trampoline from .with_effect import with_effect_ A = TypeVar('A') @@ -11,7 +11,7 @@ C = TypeVar('C') -class State(Generic[B, A], Immutable, Monad): # type: ignore +class State(Generic[B, A], Immutable, Monad): """ Represents a computation that is not yet complete, but will complete when given a state of type A diff --git a/pfun/trampoline.py b/pfun/trampoline.py index 59ac100..a2f5ebf 100644 --- a/pfun/trampoline.py +++ b/pfun/trampoline.py @@ -1,9 +1,9 @@ -from typing import Generic, TypeVar, Callable, cast, Iterable, Generator from abc import ABC, abstractmethod +from typing import Callable, Generator, Generic, Iterable, TypeVar, cast -from .immutable import Immutable -from .monad import Monad, sequence_, map_m_, filter_m_ from .curry import curry +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ from .with_effect import with_effect_ A = TypeVar('A') @@ -11,7 +11,7 @@ C = TypeVar('C') -class Trampoline(Immutable, Generic[A], Monad, ABC): # type: ignore +class Trampoline(Immutable, Generic[A], Monad, ABC): """ Base class for Trampolines. Useful for writing stack safe-safe recursive functions. @@ -63,7 +63,7 @@ def run(self) -> A: return cast(Done[A], trampoline).a -class Done(Trampoline[A]): # type: ignore +class Done(Trampoline[A]): """ Represents the result of a recursive computation. """ @@ -77,7 +77,7 @@ def _handle_cont(self, return cont(self.a) -class Call(Trampoline[A]): # type: ignore +class Call(Trampoline[A]): """ Represents a recursive call. """ @@ -91,7 +91,7 @@ def _resume(self) -> Trampoline[A]: return self.thunk() # type: ignore -class AndThen(Generic[A, B], Trampoline[B]): # type: ignore +class AndThen(Generic[A, B], Trampoline[B]): """ Represents monadic bind for trampolines as a class to avoid deep recursive calls to ``Trampoline.run`` during interpretation. diff --git a/pfun/util.py b/pfun/util.py index 0c27ad1..0477581 100644 --- a/pfun/util.py +++ b/pfun/util.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Callable, Generic, Tuple, Any +from typing import Any, Callable, Generic, Tuple, TypeVar from .immutable import Immutable @@ -25,7 +25,7 @@ def identity(v: A) -> A: Predicate = Callable[[A], bool] -class always(Generic[A], Immutable): # type: ignore +class always(Generic[A], Immutable): """ A Callable that always returns the same value regardless of the arguments @@ -44,7 +44,7 @@ def __call__(self, *args, **kwargs) -> A: return self.value -class Composition(Immutable): # type: ignore +class Composition(Immutable): functions: Tuple[Callable, ...] def __call__(self, *args, **kwargs): diff --git a/pfun/with_effect.py b/pfun/with_effect.py index 563ee9c..690b797 100644 --- a/pfun/with_effect.py +++ b/pfun/with_effect.py @@ -1,7 +1,8 @@ from functools import wraps -from pfun.monad import Monad +from typing import Any, Callable, Generator, TypeVar + from pfun.curry import curry -from typing import Generator, TypeVar, Callable, Any +from pfun.monad import Monad M = TypeVar('M', bound=Monad) diff --git a/pfun/writer.py b/pfun/writer.py index 00dd302..98b692a 100644 --- a/pfun/writer.py +++ b/pfun/writer.py @@ -1,16 +1,18 @@ -from typing import Generic, Callable, TypeVar, Iterable, cast, Generator +from typing import Callable, Generator, Generic, Iterable, TypeVar, cast + from pfun.monoid import M, append, empty -from .immutable import Immutable + from .curry import curry -from .monad import map_m_, Monad, sequence_, filter_m_ from .either import Either, Left +from .immutable import Immutable +from .monad import Monad, filter_m_, map_m_, sequence_ from .with_effect import with_effect_tail_rec A = TypeVar('A') B = TypeVar('B') -class Writer(Generic[A, M], Immutable, Monad): # type: ignore +class Writer(Generic[A, M], Immutable, Monad): """ Represents a value along with a monoid value that is accumulated as diff --git a/poetry.lock b/poetry.lock index 97ab3be..ac6397d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.5" +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + [[package]] category = "dev" description = "Disable App Nap on OS X 10.9" @@ -26,10 +34,11 @@ version = "0.1.0" [[package]] category = "dev" description = "Atomic file writes." +marker = "python_version >= \"3.5\" and sys_platform == \"win32\" or sys_platform == \"win32\"" name = "atomicwrites" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.3.0" +version = "1.4.0" [[package]] category = "dev" @@ -37,18 +46,13 @@ description = "Classes Without Boilerplate" name = "attrs" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.1.0" +version = "19.3.0" -[[package]] -category = "dev" -description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" -name = "autopep8" -optional = false -python-versions = "*" -version = "1.4.4" - -[package.dependencies] -pycodestyle = ">=2.4.0" +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] [[package]] category = "dev" @@ -56,7 +60,7 @@ description = "Internationalization utilities" name = "babel" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.7.0" +version = "2.8.0" [package.dependencies] pytz = ">=2015.7" @@ -75,7 +79,15 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2019.6.16" +version = "2020.4.5.1" + +[[package]] +category = "dev" +description = "Validate configuration and produce human readable error messages." +name = "cfgv" +optional = false +python-versions = ">=3.6.1" +version = "3.1.0" [[package]] category = "dev" @@ -88,11 +100,11 @@ version = "3.0.4" [[package]] category = "dev" description = "Cross-platform colored terminal text." -marker = "python_version >= \"3.4\" and sys_platform == \"win32\" or sys_platform == \"win32\" or python_version >= \"3.5\" and sys_platform == \"win32\"" +marker = "python_version >= \"3.4\" and sys_platform == \"win32\" or sys_platform == \"win32\" or python_version >= \"3.5\" and sys_platform == \"win32\" or platform_system == \"Windows\"" name = "colorama" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.4.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" [[package]] category = "dev" @@ -100,26 +112,37 @@ description = "Python parser for the CommonMark Markdown spec" name = "commonmark" optional = false python-versions = "*" -version = "0.9.0" +version = "0.9.1" -[package.dependencies] -future = "*" +[package.extras] +test = ["flake8 (3.7.8)", "hypothesis (3.55.3)"] [[package]] category = "dev" description = "Code coverage measurement for Python" name = "coverage" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" -version = "4.5.4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.1" + +[package.extras] +toml = ["toml"] [[package]] category = "dev" -description = "Better living through Python with decorators" +description = "Decorators for Humans" name = "decorator" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.4.0" +version = "4.4.2" + +[[package]] +category = "dev" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.3.0" [[package]] category = "dev" @@ -134,8 +157,8 @@ category = "dev" description = "Docutils -- Python Documentation Utilities" name = "docutils" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.15.2" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.16" [[package]] category = "dev" @@ -151,11 +174,14 @@ description = "execnet: rapid multi-Python deployment" name = "execnet" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.7.0" +version = "1.7.1" [package.dependencies] apipkg = ">=1.4" +[package.extras] +testing = ["pre-commit"] + [[package]] category = "dev" description = "A platform independent file lock." @@ -170,7 +196,7 @@ description = "the modular source code checker: pep8, pyflakes and co" name = "flake8" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.7.8" +version = "3.7.9" [package.dependencies] entrypoints = ">=0.3.0,<0.4.0" @@ -180,22 +206,37 @@ pyflakes = ">=2.1.0,<2.2.0" [[package]] category = "dev" -description = "Clean single-source support for Python 3 and 2" -name = "future" +description = "A library for property-based testing" +name = "hypothesis" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.17.1" +python-versions = ">=3.5.2" +version = "5.10.5" + +[package.dependencies] +attrs = ">=19.2.0" +sortedcontainers = ">=2.1.0,<3.0.0" + +[package.extras] +all = ["django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.19)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["pytz (>=2014.1)", "django (>=2.2)"] +dpcontracts = ["dpcontracts (>=0.4)"] +lark = ["lark-parser (>=0.6.5)"] +numpy = ["numpy (>=1.9.0)"] +pandas = ["pandas (>=0.19)"] +pytest = ["pytest (>=4.3)"] +pytz = ["pytz (>=2014.1)"] [[package]] category = "dev" -description = "A library for property based testing" -name = "hypothesis" +description = "File identification library for Python" +name = "identify" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "4.32.3" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "1.4.20" -[package.dependencies] -attrs = ">=16.0.0" +[package.extras] +license = ["editdistance"] [[package]] category = "dev" @@ -203,7 +244,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8" +version = "2.9" [[package]] category = "dev" @@ -211,26 +252,31 @@ description = "Getting image size from png/jpeg/jpeg2000/gif file" name = "imagesize" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.0" +version = "1.2.0" [[package]] category = "dev" description = "Read metadata from Python packages" +marker = "python_version >= \"3.5\" and python_version < \"3.8\" or python_version < \"3.8\"" name = "importlib-metadata" optional = false -python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" -version = "0.19" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.0" [package.dependencies] zipp = ">=0.5" +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + [[package]] category = "dev" description = "IPython-enabled pdb" name = "ipdb" optional = false python-versions = ">=2.7" -version = "0.12.2" +version = "0.12.3" [package.dependencies] setuptools = "*" @@ -244,8 +290,8 @@ category = "dev" description = "IPython: Productive Interactive Computing" name = "ipython" optional = false -python-versions = ">=3.5" -version = "7.7.0" +python-versions = ">=3.6" +version = "7.14.0" [package.dependencies] appnope = "*" @@ -255,11 +301,22 @@ decorator = "*" jedi = ">=0.10" pexpect = "*" pickleshare = "*" -prompt-toolkit = ">=2.0.0,<2.1.0" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" pygments = "*" setuptools = ">=18.5" traitlets = ">=4.2" +[package.extras] +all = ["nose (>=0.10.1)", "Sphinx (>=1.3)", "testpath", "nbformat", "ipywidgets", "qtconsole", "numpy (>=1.14)", "notebook", "ipyparallel", "ipykernel", "pygments", "requests", "nbconvert"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] + [[package]] category = "dev" description = "Vestigial utilities from IPython" @@ -268,28 +325,49 @@ optional = false python-versions = "*" version = "0.2.0" +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + [[package]] category = "dev" description = "An autocompletion tool for Python that can be used for text editors." name = "jedi" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.14.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.17.0" [package.dependencies] -parso = ">=0.5.0" +parso = ">=0.7.0" + +[package.extras] +qa = ["flake8 (3.7.9)"] +testing = ["colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] category = "dev" -description = "A small but fast and easy to use stand-alone template engine written in pure python." +description = "A very fast and expressive template engine." name = "jinja2" optional = false -python-versions = "*" -version = "2.10.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" [package.dependencies] MarkupSafe = ">=0.23" +[package.extras] +i18n = ["Babel (>=0.8)"] + [[package]] category = "dev" description = "A tiny library for creating CLIs" @@ -323,29 +401,40 @@ category = "dev" description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false -python-versions = ">=3.4" -version = "7.2.0" +python-versions = ">=3.5" +version = "8.2.0" [[package]] -category = "main" +category = "dev" description = "Optional static typing for Python" name = "mypy" optional = false python-versions = ">=3.5" -version = "0.740" +version = "0.770" [package.dependencies] -mypy-extensions = ">=0.4.0,<0.5.0" +mypy-extensions = ">=0.4.3,<0.5.0" typed-ast = ">=1.4.0,<1.5.0" typing-extensions = ">=3.7.4" +[package.extras] +dmypy = ["psutil (>=4.0)"] + [[package]] -category = "main" +category = "dev" description = "Experimental type system extensions for programs checked with the mypy typechecker." name = "mypy-extensions" optional = false python-versions = "*" -version = "0.4.1" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Node.js virtual environment builder" +name = "nodeenv" +optional = false +python-versions = "*" +version = "1.4.0" [[package]] category = "dev" @@ -353,10 +442,9 @@ description = "Core utilities for Python packages" name = "packaging" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.1" +version = "20.3" [package.dependencies] -attrs = "*" pyparsing = ">=2.0.2" six = "*" @@ -365,8 +453,11 @@ category = "dev" description = "A Python Parser" name = "parso" optional = false -python-versions = "*" -version = "0.5.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.7.0" + +[package.extras] +testing = ["docopt", "pytest (>=3.0.7)"] [[package]] category = "dev" @@ -375,7 +466,7 @@ marker = "python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platfor name = "pexpect" optional = false python-versions = "*" -version = "4.7.0" +version = "4.8.0" [package.dependencies] ptyprocess = ">=0.5" @@ -394,27 +485,63 @@ description = "plugin and hook calling mechanisms for python" name = "pluggy" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.12.0" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +name = "pre-commit" +optional = false +python-versions = ">=3.6.1" +version = "2.5.1" [package.dependencies] -importlib-metadata = ">=0.12" +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[[package]] +category = "dev" +description = "Some out-of-the-box hooks for pre-commit." +name = "pre-commit-hooks" +optional = false +python-versions = ">=3.6.1" +version = "3.1.0" + +[package.dependencies] +"ruamel.yaml" = ">=0.15" +toml = "*" [[package]] category = "dev" description = "Library for building powerful interactive command lines in Python" name = "prompt-toolkit" optional = false -python-versions = "*" -version = "2.0.9" +python-versions = ">=3.6.1" +version = "3.0.5" [package.dependencies] -six = ">=1.9.0" wcwidth = "*" [[package]] category = "dev" description = "Run a subprocess in a pseudo terminal" -marker = "python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\"" +marker = "python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\" or python_version >= \"3.4\" and sys_platform != \"win32\" and (python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\")" name = "ptyprocess" optional = false python-versions = "*" @@ -426,7 +553,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili name = "py" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.8.0" +version = "1.8.1" [[package]] category = "dev" @@ -449,8 +576,8 @@ category = "dev" description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.4.2" +python-versions = ">=3.5" +version = "2.6.1" [[package]] category = "dev" @@ -458,25 +585,47 @@ description = "Python parsing module" name = "pyparsing" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.2" +version = "2.4.7" [[package]] category = "dev" description = "pytest: simple powerful testing with Python" name = "pytest" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.10.1" +python-versions = ">=3.5" +version = "5.4.1" [package.dependencies] atomicwrites = ">=1.0" attrs = ">=17.4.0" colorama = "*" more-itertools = ">=4.0.0" -pluggy = ">=0.7" +packaging = "*" +pluggy = ">=0.12,<1.0" py = ">=1.5.0" -setuptools = "*" -six = ">=1.10.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "Pytest support for asyncio." +name = "pytest-asyncio" +optional = false +python-versions = ">= 3.5" +version = "0.11.0" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] [[package]] category = "dev" @@ -484,19 +633,22 @@ description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.7.1" +version = "2.8.1" [package.dependencies] coverage = ">=4.4" pytest = ">=3.6" +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] + [[package]] category = "dev" description = "pytest plugin to check FLAKE8 requirements" name = "pytest-flake8" optional = false python-versions = "*" -version = "1.0.4" +version = "1.0.5" [package.dependencies] flake8 = ">=3.5" @@ -507,8 +659,8 @@ category = "dev" description = "run tests in isolated forked subprocesses" name = "pytest-forked" optional = false -python-versions = "*" -version = "1.0.2" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.3" [package.dependencies] pytest = ">=3.1.0" @@ -532,38 +684,50 @@ version = ">=2.8" [[package]] category = "dev" -description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." -name = "pytest-sugar" +description = "pytest plugin for writing tests for mypy plugins" +name = "pytest-mypy-plugins" optional = false python-versions = "*" -version = "0.9.2" +version = "1.3.0" [package.dependencies] -packaging = ">=14.1" -pytest = ">=2.9" -termcolor = ">=1.1.0" +decorator = "*" +mypy = ">=0.730" +pytest = "*" +pyyaml = "*" [[package]] category = "dev" description = "pytest xdist plugin for distributed testing and loop-on-failing modes" name = "pytest-xdist" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.27.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.32.0" [package.dependencies] execnet = ">=1.1" -pytest = ">=3.6.0" +pytest = ">=4.4.0" pytest-forked = "*" six = "*" +[package.extras] +testing = ["filelock"] + [[package]] category = "dev" description = "World timezone definitions, modern and historical" name = "pytz" optional = false python-versions = "*" -version = "2019.2" +version = "2020.1" + +[[package]] +category = "dev" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" [[package]] category = "dev" @@ -584,29 +748,67 @@ description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.22.0" +version = "2.23.0" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "dev" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +name = "ruamel.yaml" +optional = false +python-versions = "*" +version = "0.16.10" + +[package.dependencies] +[package.dependencies."ruamel.yaml.clib"] +python = "<3.9" +version = ">=0.1.2" + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +category = "dev" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +marker = "platform_python_implementation == \"CPython\" and python_version < \"3.9\"" +name = "ruamel.yaml.clib" +optional = false +python-versions = "*" +version = "0.2.0" + [[package]] category = "dev" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.12.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" [[package]] category = "dev" -description = "This package provides 23 stemmers for 22 languages generated from Snowball algorithms." +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." name = "snowballstemmer" optional = false python-versions = "*" -version = "1.9.0" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +name = "sortedcontainers" +optional = false +python-versions = "*" +version = "2.1.0" [[package]] category = "dev" @@ -614,7 +816,7 @@ description = "Python documentation generator" name = "sphinx" optional = false python-versions = ">=3.5" -version = "2.1.2" +version = "2.4.4" [package.dependencies] Jinja2 = ">=2.3" @@ -635,40 +837,45 @@ sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = "*" -[[package]] -category = "dev" -description = "Read the Docs theme for Sphinx" -name = "sphinx-rtd-theme" -optional = false -python-versions = "*" -version = "0.4.3" - -[package.dependencies] -sphinx = "*" +[package.extras] +docs = ["sphinxcontrib-websupport"] +test = ["pytest (<5.3.3)", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"] [[package]] category = "dev" -description = "" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" name = "sphinxcontrib-applehelp" optional = false -python-versions = "*" -version = "1.0.1" +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] [[package]] category = "dev" -description = "" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." name = "sphinxcontrib-devhelp" optional = false -python-versions = "*" -version = "1.0.1" +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] [[package]] category = "dev" -description = "" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" name = "sphinxcontrib-htmlhelp" optional = false -python-versions = "*" -version = "1.0.2" +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] [[package]] category = "dev" @@ -678,21 +885,32 @@ optional = false python-versions = ">=3.5" version = "1.0.1" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "dev" -description = "" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." name = "sphinxcontrib-qthelp" optional = false -python-versions = "*" -version = "1.0.2" +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] [[package]] category = "dev" -description = "" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." name = "sphinxcontrib-serializinghtml" optional = false -python-versions = "*" -version = "1.1.3" +python-versions = ">=3.5" +version = "1.1.4" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] [[package]] category = "dev" @@ -702,14 +920,6 @@ optional = false python-versions = "*" version = "1.2.0" -[[package]] -category = "dev" -description = "ANSII Color formatting for output in terminal." -name = "termcolor" -optional = false -python-versions = "*" -version = "1.1.0" - [[package]] category = "dev" description = "Python Library for Tom's Obvious, Minimal Language" @@ -723,21 +933,26 @@ category = "dev" description = "tox is a generic virtualenv management and test command line tool" name = "tox" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.14.0" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "3.15.0" [package.dependencies] +colorama = ">=0.4.1" filelock = ">=3.0.0,<4" packaging = ">=14" pluggy = ">=0.12.0,<1" py = ">=1.4.17,<2" -six = ">=1.0.0,<2" +six = ">=1.14.0,<2" toml = ">=0.9.4" -virtualenv = ">=14.0.0" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" [package.dependencies.importlib-metadata] python = "<3.8" -version = ">=0.12,<1" +version = ">=0.12,<2" + +[package.extras] +docs = ["sphinx (>=2.0.0,<3)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] +testing = ["freezegun (>=0.3.11,<1)", "pathlib2 (>=2.3.3,<3)", "pytest (>=4.0.0,<6)", "pytest-cov (>=2.5.1,<3)", "pytest-mock (>=1.10.0,<2)", "pytest-xdist (>=1.22.2,<2)", "pytest-randomly (>=1.0.0,<4)", "flaky (>=3.4.0,<4)", "psutil (>=5.6.1,<6)"] [[package]] category = "dev" @@ -745,28 +960,23 @@ description = "Traitlets Python config system" name = "traitlets" optional = false python-versions = "*" -version = "4.3.2" +version = "4.3.3" [package.dependencies] decorator = "*" ipython-genutils = "*" six = "*" +[package.extras] +test = ["pytest", "mock"] + [[package]] -category = "main" +category = "dev" description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.0" - -[[package]] -category = "main" -description = "Type Hints for Python" -name = "typing" -optional = false -python-versions = "*" -version = "3.7.4" +version = "1.4.1" [[package]] category = "main" @@ -774,26 +984,42 @@ description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4" - -[package.dependencies] -typing = ">=3.7.4" +version = "3.7.4.2" [[package]] category = "dev" description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -version = "1.25.3" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.9" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] category = "dev" description = "Virtual Python Environment builder" name = "virtualenv" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "16.7.5" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "20.0.20" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.0,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.extras] +docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] +testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout", "packaging (>=20.0)", "xonsh (>=0.9.16)"] [[package]] category = "dev" @@ -801,7 +1027,7 @@ description = "Measures number of Terminal column cells of wide-character codes" name = "wcwidth" optional = false python-versions = "*" -version = "0.1.7" +version = "0.1.9" [[package]] category = "dev" @@ -814,94 +1040,488 @@ version = "0.28.0" [[package]] category = "dev" description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version >= \"3.5\" and python_version < \"3.8\" or python_version < \"3.8\" or python_version >= \"3.5\" and python_version < \"3.8\" and (python_version >= \"3.5\" and python_version < \"3.8\" or python_version < \"3.8\")" name = "zipp" optional = false -python-versions = ">=2.7" -version = "0.5.2" +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "7b0ae29d499fd1022e34cbaff67fc34eee643d464b9c2986238beb9a80dd0c9d" +content-hash = "1c60d061009c7cf1302fad11f8c1356c0eed434056ffba952b7d356eef87d8f7" python-versions = "^3.7" -[metadata.hashes] -alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"] -apipkg = ["37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6", "58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"] -appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] -atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] -attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] -autopep8 = ["4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee"] -babel = ["af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", "e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"] -backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"] -certifi = ["046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", "945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"] -chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] -commonmark = ["14c3df31e8c9c463377e287b2a1eefaa6019ab97b22dad36e2f32be59d61d68d", "867fc5db078ede373ab811e16b6789e9d033b15ccd7296f370ca52d1ee792ce0"] -coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] -decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] -docstring-parser = ["06e012a6e4410427423d401928709f5b10f1bd3f07d947c5dc5a27aa86df272e"] -docutils = ["6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", "9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"] -entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] -execnet = ["0dd40ad3b960aae93bdad7fe1c3f049bbcc8fba47094655a4301f5b33e906816", "3839f3c1e9270926e7b3d9b0a52a57be89c302a3826a2b19c8d6e6c3d2b506d2"] -filelock = ["18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"] -flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] -future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] -hypothesis = ["76638e3181761bd3a527e5b51b7a2ef8e30c3a3373128ba43d4462d8bf2a864f", "c6d4ba47bc97e4651fccd692d6cecca9c1ad673f114107e4d37419d5fc172ee2"] -idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] -imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"] -importlib-metadata = ["23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", "80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"] -ipdb = ["473fdd798a099765f093231a8b1fabfa95b0b682fce12de0c74b61a4b4d8ee57"] -ipython = ["1d3a1692921e932751bc1a1f7bb96dc38671eeefdc66ed33ee4cbc57e92a410e", "537cd0176ff6abd06ef3e23f2d0c4c2c8a4d9277b7451544c6cbf56d1c79a83d"] -ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", "eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"] -jedi = ["53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", "e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c"] -jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] -main-dec = ["1117b0f3c470b5f2b6d33667a1e6e6b1980d204cfaaab493c8fae4991725d1e5", "e64316e9d98f3f5f3f9d319f5a41dbc137d02014263783a7d90ba7d8030e0105"] -markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] -mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] -more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] -mypy = ["1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f", "31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00", "3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae", "48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d", "540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9", "672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391", "6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f", "9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9", "ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990", "b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7", "d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb", "d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453", "dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b", "f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb"] -mypy-extensions = ["37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", "b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"] -packaging = ["a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", "c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"] -parso = ["63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", "666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"] -pexpect = ["2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", "9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"] -pickleshare = ["87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", "9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"] -pluggy = ["0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", "b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"] -prompt-toolkit = ["11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", "2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", "977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"] -ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] -py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] -pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] -pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] -pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] -pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] -pytest = ["3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", "e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"] -pytest-cov = ["2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", "e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"] -pytest-flake8 = ["4d225c13e787471502ff94409dcf6f7927049b2ec251c63b764a4b17447b60c0", "d7e2b6b274a255b7ae35e9224c85294b471a83b76ecb6bd53c337ae977a499af"] -pytest-forked = ["5fe33fbd07d7b1302c95310803a5e5726a4ff7f19d5a542b7ce57c76fed8135f", "d352aaced2ebd54d42a65825722cb433004b4446ab5d2044851d9cc7a00c9e38"] -pytest-mypy = ["419d1d4877d41a6a80f0eb31faa7c50bb9445557f7ff1b02a1a26d10d7dc7691", "917438af835beb87f14c9f6261137f8e992b3bf87ebf73f836ac7ede03424a0f"] -pytest-sugar = ["26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283", "fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab"] -pytest-xdist = ["a64915be2b23235d6cec0992b8f59b791d64083756fbf13cf574fa5757085bc7", "a96ed691705882560fa3fc95531fbd4c224896c827f4004817eb2dcac4ba41a2"] -pytz = ["26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", "c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"] -recommonmark = ["a520b8d25071a51ae23a27cf6252f2fe387f51bdc913390d83b2b50617f5bb48", "c85228b9b7aea7157662520e74b4e8791c5eacd375332ec68381b52bf10165be"] -requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] -six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] -snowballstemmer = ["9f3b9ffe0809d174f7047e121431acf99c89a7040f0ca84f94ba53a498e6d0c9"] -sphinx = ["22538e1bbe62b407cf5a8aabe1bb15848aa66bb79559f42f5202bbce6b757a69", "f9a79e746b87921cabc3baa375199c6076d1270cee53915dbd24fdbeaaacc427"] -sphinx-rtd-theme = ["00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", "728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"] -sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"] -sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"] -sphinxcontrib-htmlhelp = ["4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"] -sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"] -sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"] -sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"] -stringcase = ["48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"] -termcolor = ["1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"] -toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] -tox = ["0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e", "c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1"] -traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] -typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] -typing = ["38566c558a0a94d6531012c8e917b1b8518a41e418f7f15f00e129cc80162ad3", "53765ec4f83a2b720214727e319607879fec4acde22c4fbb54fa2604e79e44ce", "84698954b4e6719e912ef9a42a2431407fe3755590831699debda6fba92aac55"] -typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"] -urllib3 = ["b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", "dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"] -virtualenv = ["680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", "f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2"] -wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] -yapf = ["02ace10a00fa2e36c7ebd1df2ead91dbfbd7989686dc4ccbdc549e95d19f5780", "6f94b6a176a7c114cfa6bad86d40f259bbe0f10cf2fa7f2f4b3596fc5802a41b"] -zipp = ["4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", "8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec"] +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +apipkg = [ + {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, + {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, +] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +appnope = [ + {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, + {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] +backcall = [ + {file = "backcall-0.1.0.tar.gz", hash = "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4"}, + {file = "backcall-0.1.0.zip", hash = "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"}, +] +certifi = [ + {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, + {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, +] +cfgv = [ + {file = "cfgv-3.1.0-py2.py3-none-any.whl", hash = "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53"}, + {file = "cfgv-3.1.0.tar.gz", hash = "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +coverage = [ + {file = "coverage-5.1-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65"}, + {file = "coverage-5.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6"}, + {file = "coverage-5.1-cp27-cp27m-win32.whl", hash = "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796"}, + {file = "coverage-5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a"}, + {file = "coverage-5.1-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768"}, + {file = "coverage-5.1-cp35-cp35m-win32.whl", hash = "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2"}, + {file = "coverage-5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7"}, + {file = "coverage-5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c"}, + {file = "coverage-5.1-cp36-cp36m-win32.whl", hash = "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1"}, + {file = "coverage-5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7"}, + {file = "coverage-5.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd"}, + {file = "coverage-5.1-cp37-cp37m-win32.whl", hash = "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e"}, + {file = "coverage-5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a"}, + {file = "coverage-5.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef"}, + {file = "coverage-5.1-cp38-cp38-win32.whl", hash = "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24"}, + {file = "coverage-5.1-cp38-cp38-win_amd64.whl", hash = "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0"}, + {file = "coverage-5.1-cp39-cp39-win32.whl", hash = "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4"}, + {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, + {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, +] +decorator = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] +distlib = [ + {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, +] +docstring-parser = [ + {file = "docstring_parser-0.3.tar.gz", hash = "sha256:06e012a6e4410427423d401928709f5b10f1bd3f07d947c5dc5a27aa86df272e"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] +execnet = [ + {file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"}, + {file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +flake8 = [ + {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, + {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"}, +] +hypothesis = [ + {file = "hypothesis-5.10.5-py3-none-any.whl", hash = "sha256:579dbe113a5cf5e3d6cf406b2afd07ad754c97be5542fa35196587eaf88c0cbe"}, + {file = "hypothesis-5.10.5.tar.gz", hash = "sha256:8f8c1c2e0ff5b6125d2fd3a47234807690f25117f989019200e19204bce3120a"}, +] +identify = [ + {file = "identify-1.4.20-py2.py3-none-any.whl", hash = "sha256:acf0712ab4042642e8f44e9532d95c26fbe60c0ab8b6e5b654dd1bc6512810e0"}, + {file = "identify-1.4.20.tar.gz", hash = "sha256:b2cd24dece806707e0b50517c1b3bcf3044e0b1cb13a72e7d34aa31c91f2a55a"}, +] +idna = [ + {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, + {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, +] +ipdb = [ + {file = "ipdb-0.12.3.tar.gz", hash = "sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd"}, +] +ipython = [ + {file = "ipython-7.14.0-py3-none-any.whl", hash = "sha256:5b241b84bbf0eb085d43ae9d46adf38a13b45929ca7774a740990c2c242534bb"}, + {file = "ipython-7.14.0.tar.gz", hash = "sha256:f0126781d0f959da852fb3089e170ed807388e986a8dd4e6ac44855845b0fb1c"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +jedi = [ + {file = "jedi-0.17.0-py2.py3-none-any.whl", hash = "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798"}, + {file = "jedi-0.17.0.tar.gz", hash = "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +main-dec = [ + {file = "main-dec-0.1.1.tar.gz", hash = "sha256:e64316e9d98f3f5f3f9d319f5a41dbc137d02014263783a7d90ba7d8030e0105"}, + {file = "main_dec-0.1.1-py3-none-any.whl", hash = "sha256:1117b0f3c470b5f2b6d33667a1e6e6b1980d204cfaaab493c8fae4991725d1e5"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, + {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, +] +mypy = [ + {file = "mypy-0.770-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600"}, + {file = "mypy-0.770-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754"}, + {file = "mypy-0.770-cp35-cp35m-win_amd64.whl", hash = "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65"}, + {file = "mypy-0.770-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce"}, + {file = "mypy-0.770-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761"}, + {file = "mypy-0.770-cp36-cp36m-win_amd64.whl", hash = "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2"}, + {file = "mypy-0.770-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8"}, + {file = "mypy-0.770-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913"}, + {file = "mypy-0.770-cp37-cp37m-win_amd64.whl", hash = "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9"}, + {file = "mypy-0.770-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1"}, + {file = "mypy-0.770-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27"}, + {file = "mypy-0.770-cp38-cp38-win_amd64.whl", hash = "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"}, + {file = "mypy-0.770-py3-none-any.whl", hash = "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164"}, + {file = "mypy-0.770.tar.gz", hash = "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, +] +packaging = [ + {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, + {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, +] +parso = [ + {file = "parso-0.7.0-py2.py3-none-any.whl", hash = "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0"}, + {file = "parso-0.7.0.tar.gz", hash = "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +pre-commit = [ + {file = "pre_commit-2.5.1-py2.py3-none-any.whl", hash = "sha256:c5c8fd4d0e1c363723aaf0a8f9cba0f434c160b48c4028f4bae6d219177945b3"}, + {file = "pre_commit-2.5.1.tar.gz", hash = "sha256:da463cf8f0e257f9af49047ba514f6b90dbd9b4f92f4c8847a3ccd36834874c7"}, +] +pre-commit-hooks = [ + {file = "pre_commit_hooks-3.1.0-py2.py3-none-any.whl", hash = "sha256:32e07d6bd511e26ac3d7b7aafdd2e49d0f1efdb7fc772156386004b9e6f66dbe"}, + {file = "pre_commit_hooks-3.1.0.tar.gz", hash = "sha256:78642bdda65d524a6c91faaf4b322f18fc561e4377e8651d8502c6073e4a19d9"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.5-py3-none-any.whl", hash = "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04"}, + {file = "prompt_toolkit-3.0.5.tar.gz", hash = "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8"}, +] +ptyprocess = [ + {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, + {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, +] +py = [ + {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, + {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, +] +pycodestyle = [ + {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, + {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, +] +pyflakes = [ + {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, + {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, +] +pygments = [ + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"}, + {file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.11.0.tar.gz", hash = "sha256:c54866f3cf5dd2063992ba2c34784edae11d3ed19e006d220a3cf0bfc4191fcb"}, + {file = "pytest_asyncio-0.11.0-py3-none-any.whl", hash = "sha256:6096d101a1ae350d971df05e25f4a8b4d3cd13ffb1b32e42d902ac49670d2bfa"}, +] +pytest-cov = [ + {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, + {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, +] +pytest-flake8 = [ + {file = "pytest-flake8-1.0.5.tar.gz", hash = "sha256:d85efaafbdb9580791cfa8671799dd40d482fc30bd4476c1ca5efd661e751333"}, + {file = "pytest_flake8-1.0.5-py2.py3-none-any.whl", hash = "sha256:6e26d94ad41184d9a5113a90179b303efddb53eda505f827418ca78f5b39403a"}, +] +pytest-forked = [ + {file = "pytest-forked-1.1.3.tar.gz", hash = "sha256:1805699ed9c9e60cb7a8179b8d4fa2b8898098e82d229b0825d8095f0f261100"}, + {file = "pytest_forked-1.1.3-py2.py3-none-any.whl", hash = "sha256:1ae25dba8ee2e56fb47311c9638f9e58552691da87e82d25b0ce0e4bf52b7d87"}, +] +pytest-mypy = [ + {file = "pytest-mypy-0.3.3.tar.gz", hash = "sha256:917438af835beb87f14c9f6261137f8e992b3bf87ebf73f836ac7ede03424a0f"}, + {file = "pytest_mypy-0.3.3-py3-none-any.whl", hash = "sha256:419d1d4877d41a6a80f0eb31faa7c50bb9445557f7ff1b02a1a26d10d7dc7691"}, +] +pytest-mypy-plugins = [ + {file = "pytest-mypy-plugins-1.3.0.tar.gz", hash = "sha256:7920a38520f914e849cafc81582318673bec8924ff71ee2d0f98dc738995d06d"}, + {file = "pytest_mypy_plugins-1.3.0-py3-none-any.whl", hash = "sha256:db18943f8775260308a11948a42739c6789bbab8a1bafb64103e4429ce31dfc0"}, +] +pytest-xdist = [ + {file = "pytest-xdist-1.32.0.tar.gz", hash = "sha256:1d4166dcac69adb38eeaedb88c8fada8588348258a3492ab49ba9161f2971129"}, + {file = "pytest_xdist-1.32.0-py2.py3-none-any.whl", hash = "sha256:ba5ec9fde3410bd9a116ff7e4f26c92e02fa3d27975ef3ad03f330b3d4b54e91"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +recommonmark = [ + {file = "recommonmark-0.5.0-py2.py3-none-any.whl", hash = "sha256:c85228b9b7aea7157662520e74b4e8791c5eacd375332ec68381b52bf10165be"}, + {file = "recommonmark-0.5.0.tar.gz", hash = "sha256:a520b8d25071a51ae23a27cf6252f2fe387f51bdc913390d83b2b50617f5bb48"}, +] +requests = [ + {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, + {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, +] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.16.10-py2.py3-none-any.whl", hash = "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b"}, + {file = "ruamel.yaml-0.16.10.tar.gz", hash = "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win32.whl", hash = "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win32.whl", hash = "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win32.whl", hash = "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win32.whl", hash = "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070"}, + {file = "ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30"}, + {file = "ruamel.yaml.clib-0.2.0.tar.gz", hash = "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c"}, +] +six = [ + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sortedcontainers = [ + {file = "sortedcontainers-2.1.0-py2.py3-none-any.whl", hash = "sha256:d9e96492dd51fae31e60837736b38fe42a187b5404c16606ff7ee7cd582d4c60"}, + {file = "sortedcontainers-2.1.0.tar.gz", hash = "sha256:974e9a32f56b17c1bac2aebd9dcf197f3eb9cd30553c5852a3187ad162e1a03a"}, +] +sphinx = [ + {file = "Sphinx-2.4.4-py3-none-any.whl", hash = "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"}, + {file = "Sphinx-2.4.4.tar.gz", hash = "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] +stringcase = [ + {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +tox = [ + {file = "tox-3.15.0-py2.py3-none-any.whl", hash = "sha256:8d97bfaf70053ed3db56f57377288621f1bcc7621446d301927d18df93b1c4c3"}, + {file = "tox-3.15.0.tar.gz", hash = "sha256:af09c19478e8fc7ce7555b3d802ddf601b82684b874812c5857f774b8aee1b67"}, +] +traitlets = [ + {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, + {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, + {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, + {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, +] +urllib3 = [ + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, +] +virtualenv = [ + {file = "virtualenv-20.0.20-py2.py3-none-any.whl", hash = "sha256:b4c14d4d73a0c23db267095383c4276ef60e161f94fde0427f2f21a0132dde74"}, + {file = "virtualenv-20.0.20.tar.gz", hash = "sha256:fd0e54dec8ac96c1c7c87daba85f0a59a7c37fe38748e154306ca21c73244637"}, +] +wcwidth = [ + {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, + {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, +] +yapf = [ + {file = "yapf-0.28.0-py2.py3-none-any.whl", hash = "sha256:02ace10a00fa2e36c7ebd1df2ead91dbfbd7989686dc4ccbdc549e95d19f5780"}, + {file = "yapf-0.28.0.tar.gz", hash = "sha256:6f94b6a176a7c114cfa6bad86d40f259bbe0f10cf2fa7f2f4b3596fc5802a41b"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/pyproject.toml b/pyproject.toml index aab042c..022e469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pfun" -version = "0.5.1" +version = "0.6.0" description = "" authors = ["Sune Debel "] readme = "README.md" @@ -8,26 +8,27 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.7" typing-extensions = "^3.7" -mypy = "^0.740" [tool.poetry.dev-dependencies] -pytest = "^3.0" -hypothesis = "^4.32" -mypy = "^0.740.0" +pytest-mypy-plugins = "^1.3" +pytest-asyncio = "^0.11.0" +pytest = "^5.4.1" +hypothesis = "^5.10.4" ipython = "^7.7" ipdb = "^0.12.2" sphinx = "^2.1" -pytest-sugar = "^0.9.2" pytest-cov = "^2.7" recommonmark = "^0.5.0" -sphinx-rtd-theme = "^0.4.3" flake8 = "^3.7" -pytest-xdist = "1.27" -autopep8 = "^1.4" +pytest-xdist = "1.32" yapf = "^0.28.0" pytest-flake8 = "^1.0" pytest-mypy = "^0.3.3" tox = "^3.14" +mypy = "0.770" +isort = "^4.3.21" +pre-commit = "^2.5.1" +pre-commit-hooks = "^3.1.0" main-dec = "^0.1.1" [build-system] diff --git a/scripts/check_version.py b/scripts/check_version.py index de74563..f579c9d 100644 --- a/scripts/check_version.py +++ b/scripts/check_version.py @@ -2,7 +2,7 @@ from main_dec import main -from pfun.io import read_str, with_effect, IOs +from pfun.io import IOs, read_str, with_effect class MalformedTomlError(Exception): diff --git a/setup.cfg b/setup.cfg index 059e479..07126a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,7 @@ plugins = pfun.mypy_plugin warn_redundant_casts = True warn_unused_ignores = True +#check_untyped_defs = True [flake8] ignore = E741,W504,F811,E731,F821 @@ -13,9 +14,7 @@ dedent_closing_brackets = true split_all_top_level_comma_separated_values = true split_before_dot = false join_multiple_lines = false - -[mypy-tests.type_tests.negatives.*] -ignore_errors = True +column_limit = 79 [mypy-pytest] ignore_missing_imports = True diff --git a/tests/monad_test.py b/tests/monad_test.py index cd81658..8f13764 100644 --- a/tests/monad_test.py +++ b/tests/monad_test.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod + from .functor_test import FunctorTest diff --git a/tests/strategies.py b/tests/strategies.py index 47692b5..db92f22 100644 --- a/tests/strategies.py +++ b/tests/strategies.py @@ -1,32 +1,14 @@ -from pfun import ( - maybe, List, reader, state, Dict, cont, writer, trampoline, free -) -from hypothesis.strategies import ( - integers, - booleans, - text, - one_of, - floats, - builds, - just, - lists as lists_, - dictionaries, - tuples, - none, - composite, - binary -) +from hypothesis.strategies import (binary, booleans, builds, composite, + dictionaries, floats, integers, just) +from hypothesis.strategies import lists as lists_ +from hypothesis.strategies import none, one_of, text, tuples +from pfun import (Dict, List, aio_trampoline, cont, effect, free, maybe, + reader, state, trampoline, writer) from pfun.either import Left, Right -from pfun.io import ( - value as IO, - read_bytes, - read_str, - put_line, - get_line, - write_bytes, - write_str -) +from pfun.io import get_line, put_line, read_bytes, read_str +from pfun.io import value as IO +from pfun.io import write_bytes, write_str def _everything(allow_nan=False): @@ -91,6 +73,28 @@ def and_then(draw): return one_of(dones, call(), and_then()) +def aio_trampolines(value_strategy=anything()): + dones = builds(aio_trampoline.Done, value_strategy) + + @composite + def call(draw): + t = draw(aio_trampolines(value_strategy)) + + async def f(): + return t + return aio_trampoline.Call(f) + + @composite + def and_then(draw): + t = draw(aio_trampolines(value_strategy)) + cont = lambda _: t + return aio_trampoline.AndThen( + draw(aio_trampolines(value_strategy)), cont + ) + + return one_of(dones, call(), and_then()) + + def lists(element_strategies=_everything(allow_nan=False), min_size=0): return builds( List, @@ -169,3 +173,7 @@ def ios(value_strategy=anything()): gets(), puts() ) + + +def effects(value_strategy=anything()): + return builds(effect.success, value_strategy) diff --git a/tests/test_aio_trampoline.py b/tests/test_aio_trampoline.py new file mode 100644 index 0000000..abaa106 --- /dev/null +++ b/tests/test_aio_trampoline.py @@ -0,0 +1,53 @@ +import pytest +from hypothesis import assume, given + +from pfun import compose, identity +from pfun.aio_trampoline import Done + +from .monad_test import MonadTest +from .strategies import aio_trampolines, anything, unaries + + +class TestTrampoline(MonadTest): + @pytest.mark.asyncio + @given(aio_trampolines()) + async def test_right_identity_law(self, trampoline): + assert (await + trampoline.and_then(Done).run()) == (await trampoline.run()) + + @pytest.mark.asyncio + @given(anything(), unaries(aio_trampolines())) + async def test_left_identity_law(self, value, f): + assert (await Done(value).and_then(f).run()) == (await f(value).run()) + + @pytest.mark.asyncio + @given( + aio_trampolines(), + unaries(aio_trampolines()), + unaries(aio_trampolines()) + ) + async def test_associativity_law(self, trampoline, f, g): + assert (await trampoline.and_then(f).and_then(g).run( + )) == (await trampoline.and_then(lambda x: f(x).and_then(g)).run()) + + @given(anything()) + def test_equality(self, value): + assert Done(value) == Done(value) + + @given(anything(), anything()) + def test_inequality(self, first, second): + assume(first != second) + assert Done(first) != Done(second) + + @pytest.mark.asyncio + @given(anything()) + async def test_identity_law(self, value): + assert (await + Done(value).map(identity).run()) == (await Done(value).run()) + + @pytest.mark.asyncio + @given(unaries(), unaries(), anything()) + async def test_composition_law(self, f, g, value): + h = compose(f, g) + assert (await Done(value).map(g).map(f).run() + ) == (await Done(value).map(h).run()) diff --git a/tests/test_cont.py b/tests/test_cont.py index 7458698..7c8d872 100644 --- a/tests/test_cont.py +++ b/tests/test_cont.py @@ -1,8 +1,9 @@ -from hypothesis import given, assume +from hypothesis import assume, given -from pfun import cont, identity, compose +from pfun import compose, cont, identity from tests.monad_test import MonadTest -from tests.strategies import anything, unaries, conts +from tests.strategies import anything, conts, unaries + from .utils import recursion_limit diff --git a/tests/test_curry.py b/tests/test_curry.py index 5508383..0e5c7c8 100644 --- a/tests/test_curry.py +++ b/tests/test_curry.py @@ -1,5 +1,5 @@ import pytest -from hypothesis import given, assume +from hypothesis import assume, given from hypothesis.strategies import integers from pfun import curry diff --git a/tests/test_dict.py b/tests/test_dict.py index 23c0c1a..7fa0fb8 100644 --- a/tests/test_dict.py +++ b/tests/test_dict.py @@ -1,4 +1,4 @@ -from hypothesis import given, assume +from hypothesis import assume, given from pfun import Dict from pfun.maybe import Just, Nothing diff --git a/tests/test_effect.py b/tests/test_effect.py new file mode 100644 index 0000000..85372fc --- /dev/null +++ b/tests/test_effect.py @@ -0,0 +1,292 @@ +from subprocess import CalledProcessError +from unittest import mock + +import pytest +from hypothesis import assume, given + +from pfun import compose, effect, either, identity + +from .monad_test import MonadTest +from .strategies import anything, effects, unaries +from .utils import recursion_limit + + +class TestEffect(MonadTest): + @given(effects(), unaries(effects()), unaries(effects()), anything()) + def test_associativity_law(self, e, f, g, env): + assert ( + e.and_then(f).and_then(g).run(env) == + e.and_then(lambda x: f(x).and_then(g)).run(env) + ) + + @given(unaries(), unaries(), anything(), anything()) + def test_composition_law(self, f, g, value, env): + h = compose(f, g) + assert ( + effect.success(value).map(h).run(env) == + effect.success(value).map(g).map(f).run(env) + ) + + @given(anything(), anything()) + def test_identity_law(self, value, env): + assert ( + effect.success(value).map(identity).run(env) == + effect.success(value).run(env) + ) + + @given(unaries(effects()), anything(), anything()) + def test_left_identity_law(self, f, value, env): + assert ( + effect.success(value).and_then(f).run(env) == f(value).run(env) + ) + + @given(anything(), anything()) + def test_right_identity_law(self, value, env): + assert ( + effect.success(value).and_then( + effect.success + ).run(env) == effect.success(value).run(env) + ) + + @given(anything(), anything()) + def test_equality(self, value, env): + assert effect.success(value).run(env) == effect.success(value).run(env) + + @given(anything(), anything(), anything()) + def test_inequality(self, first, second, env): + assume(first != second) + assert effect.success(first).run(env) != effect.success(second + ).run(env) + + def test_get_environment(self): + assert effect.get_environment().run('env') == 'env' + + def test_from_awaitable(self): + async def f(): + return 1 + + assert effect.from_awaitable(f()).run(None) == 1 + + def test_sequence(self): + assert effect.sequence_async([effect.success(v) for v in range(3)] + ).run(None) == (0, 1, 2) + + def test_stack_safety(self): + with recursion_limit(100): + effect.sequence_async([effect.success(v) + for v in range(500)]).run(None) + + e = effect.error('') + for _ in range(500): + e = e.recover(lambda _: effect.error('')) + e = e.recover(lambda _: effect.success('')) + with recursion_limit(100): + e.run(None) + + e = effect.success('') + for _ in range(500): + e = e.either() + with recursion_limit(100): + e.run(None) + + def test_filter_m(self): + assert effect.filter_m(lambda v: effect.success(v % 2 == 0), + range(5)).run(None) == (0, 2, 4) + + def test_map_m(self): + assert effect.map_m(effect.success, range(3)).run(None) == (0, 1, 2) + + def test_with_effect(self): + @effect.with_effect + def f(): + a = yield effect.success(2) + b = yield effect.success(2) + return a + b + + @effect.with_effect + def test_stack_safety(): + for _ in range(500): + yield effect.success(1) + return None + + with recursion_limit(100): + test_stack_safety().run(None) + + assert f().run(None) == 4 + + def test_either(self): + success = effect.success(1) + error = effect.error('error') + assert success.either().run(None) == either.Right(1) + error.either().run(None) == either.Left('error') + + def test_recover(self): + success = effect.success(1) + error = effect.error('error') + assert success.recover(lambda e: effect.success(2)).run(None) == 1 + assert error.recover(lambda e: effect.success(2)).run(None) == 2 + + def test_absolve(self): + right = either.Right(1) + left = either.Left('error') + right_effect = effect.success(right) + left_effect = effect.success(left) + assert effect.absolve(right_effect).run(None) == 1 + with pytest.raises(Exception): + # todo + effect.absolve(left_effect).run(None) + + def test_error(self): + with pytest.raises(Exception): + # todo + effect.error('error').run(None) + + def test_combine(self): + def f(a, b): + return a + b + + assert effect.combine(effect.success('a'), + effect.success('b'))(f).run(None) == 'ab' + + def test_catch(self): + def f(fail): + if fail: + raise ValueError() + else: + return 1 + + assert effect.catch(ValueError)(lambda: f(False)).run(None) == 1 + catched_error = effect.catch(ValueError)(lambda: f(True)) + with pytest.raises(Exception): + # todo + catched_error.run(None) + with pytest.raises(ValueError): + effect.catch(ZeroDivisionError)(lambda: f(True)) + + def test_catch_all(self): + def f(value_error): + if value_error: + raise ValueError() + else: + raise ZeroDivisionError() + + catched_value_error = effect.catch_all(lambda: f(True)) + catched_division_error = effect.catch_all(lambda: f(False)) + with pytest.raises(Exception): + # todo + catched_value_error.run(None) + + with pytest.raises(Exception): + # todo + catched_division_error.run(None) + + +class HasConsole: + console = effect.console.Console() + + +def mock_open(read_data=None): + return mock.patch( + 'pfun.effect.files.open', mock.mock_open(read_data=read_data) + ) + + +class TestConsole: + def test_print_line(self, capsys) -> None: + + e = effect.console.print_line('Hello, world!') + e.run(HasConsole()) + captured = capsys.readouterr() + assert captured.out == 'Hello, world!\n' + + def test_get_line(self) -> None: + with mock.patch( + 'pfun.effect.console.input', return_value='Hello!' + ) as mocked_input: + e = effect.console.get_line('Say hello') + assert e.run(HasConsole()) == 'Hello!' + mocked_input.assert_called_once_with('Say hello') + + +class HasFiles: + files = effect.files.Files() + + +class TestFiles: + def test_read(self): + with mock_open('content'): + e = effect.files.read('foo.txt') + assert e.run(HasFiles()) == 'content' + + def test_write(self): + with mock_open() as mocked_open: + e = effect.files.write('foo.txt', 'content') + e.run(HasFiles()) + mocked_open.assert_called_once_with('foo.txt', 'w') + mocked_open().write.assert_called_once_with('content') + + def test_read_bytes(self): + with mock_open(b'content'): + e = effect.files.read_bytes('foo.txt') + assert e.run(HasFiles()) == b'content' + + def test_write_bytes(self): + with mock_open() as mocked_open: + e = effect.files.write_bytes('foo.txt', b'content') + e.run(HasFiles()) + mocked_open.assert_called_once_with('foo.txt', 'wb') + mocked_open().write.assert_called_once_with(b'content') + + def test_append(self): + with mock_open() as mocked_open: + e = effect.files.append('foo.txt', 'content') + e.run(HasFiles()) + mocked_open.assert_called_once_with('foo.txt', 'a+') + mocked_open().write.assert_called_once_with('content') + + def test_append_bytes(self): + with mock_open() as mocked_open: + e = effect.files.append_bytes('foo.txt', b'content') + e.run(HasFiles()) + mocked_open.assert_called_once_with('foo.txt', 'ab+') + mocked_open().write.assert_called_once_with(b'content') + + +class TestRef: + def test_get(self): + ref = effect.ref.Ref(0) + assert ref.get().run(None) == 0 + + def test_put(self): + ref = effect.ref.Ref(0) + ref.put(1).run(None) + assert ref.value == 1 + + def test_modify(self): + ref = effect.ref.Ref(0) + ref.modify(lambda _: 1).run(None) + assert ref.value == 1 + + def test_try_modify(self): + ref = effect.ref.Ref(0) + ref.try_modify(lambda _: either.Left('')).either().run(None) + assert ref.value == 0 + ref.try_modify(lambda _: either.Right(1)).run(None) + assert ref.value == 1 + + +class HasSubprocess: + subprocess = effect.subprocess.Subprocess() + + +class TestSubprocess: + def test_run_in_shell(self): + stdout, stderr = effect.subprocess.run_in_shell( + 'echo "test"' + ).run( + HasSubprocess() + ) + assert stdout == b'test\n' + + with pytest.raises(CalledProcessError): + effect.subprocess.run_in_shell('exit 1').run(HasSubprocess()) diff --git a/tests/test_either.py b/tests/test_either.py index b28d56e..7601f47 100644 --- a/tests/test_either.py +++ b/tests/test_either.py @@ -1,13 +1,13 @@ from typing import Any -from hypothesis import given, assume +from hypothesis import assume, given -from pfun import Unary, identity, compose -from pfun.either import ( - Either, Left, Right, either, with_effect, sequence, filter_m, map_m -) +from pfun import Unary, compose, identity +from pfun.either import (Either, Left, Right, either, filter_m, map_m, + sequence, with_effect) from tests.monad_test import MonadTest -from tests.strategies import eithers, unaries, anything +from tests.strategies import anything, eithers, unaries + from .utils import recursion_limit @@ -25,7 +25,9 @@ def test_associativity_law( self, either: Either, f: Unary[Any, Either], g: Unary[Any, Either] ): - assert either.and_then(f).and_then(g) == either.and_then( + assert either.and_then(f).and_then( + g + ) == either.and_then( # type: ignore lambda x: f(x).and_then(g) ) diff --git a/tests/test_free.py b/tests/test_free.py index 0ec082b..b162c1f 100644 --- a/tests/test_free.py +++ b/tests/test_free.py @@ -1,9 +1,10 @@ -from hypothesis import given, assume -from pfun.free import Done, with_effect, sequence, map_m, filter_m, with_effect -from pfun import identity, compose +from hypothesis import assume, given + +from pfun import compose, identity +from pfun.free import Done, filter_m, map_m, sequence, with_effect -from .strategies import frees, unaries, anything from .monad_test import MonadTest +from .strategies import anything, frees, unaries from .utils import recursion_limit diff --git a/tests/test_immutable.py b/tests/test_immutable.py index b04e463..b62a0c0 100644 --- a/tests/test_immutable.py +++ b/tests/test_immutable.py @@ -1,8 +1,9 @@ -import pytest -from hypothesis import given from dataclasses import FrozenInstanceError from typing import Any +import pytest +from hypothesis import given + from pfun import Immutable from tests.strategies import anything diff --git a/tests/test_io.py b/tests/test_io.py index c41f328..bcaca73 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,23 +1,22 @@ -from unittest.mock import patch, mock_open as mock_open_ import sys -from pfun.io import ( - put_line, - get_line, - read_str as read_file, - read_bytes as read_file_bytes, - write_str as write_file, - write_bytes as write_file_bytes, - value as IO, - with_effect, - sequence, - filter_m, - map_m -) -from pfun import identity, compose -from .monad_test import MonadTest -from .strategies import ios, unaries, anything -from hypothesis import given, assume +from unittest.mock import mock_open as mock_open_ +from unittest.mock import patch + +from hypothesis import assume, given from hypothesis.strategies import text + +from pfun import compose, identity +from pfun.io import filter_m, get_line, map_m, put_line +from pfun.io import read_bytes as read_file_bytes +from pfun.io import read_str as read_file +from pfun.io import sequence +from pfun.io import value as IO +from pfun.io import with_effect +from pfun.io import write_bytes as write_file_bytes +from pfun.io import write_str as write_file + +from .monad_test import MonadTest +from .strategies import anything, ios, unaries from .utils import recursion_limit diff --git a/tests/test_list.py b/tests/test_list.py index 6e799c9..ed4316e 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -1,21 +1,23 @@ import random import pytest +from hypothesis import assume, given +from hypothesis.strategies import integers +from hypothesis.strategies import lists as lists_ + +from pfun import List, compose, identity +from pfun.list import filter_m, map_m, sequence, value, with_effect -from pfun import List, identity, compose -from pfun.list import with_effect, sequence, filter_m, map_m, value -from hypothesis.strategies import integers, lists as lists_ -from hypothesis import given, assume -from .strategies import anything, unaries, lists from .monad_test import MonadTest from .monoid_test import MonoidTest +from .strategies import anything, lists, unaries from .utils import recursion_limit class TestList(MonadTest, MonoidTest): - @given(lists(), lists()) + @given(lists(), anything()) def test_append(self, l1, l2): - assert l1.append(l2) == l1 + l2 + assert l1.append(l2) == l1 + (l2,) def test_empty(self): assert List().empty() == List() diff --git a/tests/test_maybe.py b/tests/test_maybe.py index 91bce67..101afb5 100644 --- a/tests/test_maybe.py +++ b/tests/test_maybe.py @@ -1,19 +1,13 @@ from typing import Any + from hypothesis import assume, given -from pfun import Unary, identity, compose, List -from pfun.maybe import ( - Maybe, - Just, - Nothing, - maybe, - flatten, - with_effect, - sequence, - map_m, - filter_m -) + +from pfun import List, Unary, compose, identity +from pfun.maybe import (Just, Maybe, Nothing, filter_m, flatten, map_m, maybe, + sequence, with_effect) + from .monad_test import MonadTest -from .strategies import anything, unaries, maybes, lists +from .strategies import anything, lists, maybes, unaries from .utils import recursion_limit @@ -42,7 +36,7 @@ def test_left_identity_law(self, value, f: Unary[Any, Maybe]): def test_associativity_law( self, maybe: Maybe, f: Unary[Any, Maybe], g: Unary[Any, Maybe] ): - assert maybe.and_then(f).and_then(g) == maybe.and_then( + assert maybe.and_then(f).and_then(g) == maybe.and_then( # type: ignore lambda x: f(x).and_then(g) ) diff --git a/tests/test_monoid.py b/tests/test_monoid.py index eb48d28..dfef0ba 100644 --- a/tests/test_monoid.py +++ b/tests/test_monoid.py @@ -1,7 +1,7 @@ from hypothesis import given -from hypothesis.strategies import lists, integers, text, none, builds, tuples +from hypothesis.strategies import builds, integers, lists, none, text, tuples -from pfun.monoid import empty, append, Monoid +from pfun.monoid import Monoid, append, empty from tests.strategies import anything @@ -9,7 +9,7 @@ class M(Monoid): def __init__(self, i): self.i = i - def append(self, other: 'M') -> 'M': + def __add__(self, other: 'M') -> 'M': return M(self.i + other.i) def empty(self) -> 'M': diff --git a/tests/test_mypy_plugin.yaml b/tests/test_mypy_plugin.yaml new file mode 100644 index 0000000..7918838 --- /dev/null +++ b/tests/test_mypy_plugin.yaml @@ -0,0 +1,204 @@ +- case: variadic_decorators_preserve_nullary_signature + main: | + from pfun.maybe import maybe + + @maybe + def f() -> int: + pass + + reveal_type(f) # N: Revealed type is 'def () -> Union[pfun.maybe.Nothing, pfun.maybe.Just[builtins.int]]' +- case: variadic_decorators_preserve_unary_signature + main: | + from pfun.maybe import maybe + + @maybe + def f(a: int) -> int: + pass + + reveal_type(f) # N: Revealed type is 'def (a: builtins.int) -> Union[pfun.maybe.Nothing, pfun.maybe.Just[builtins.int]]' +- case: variadic_decorators_preserve_binary_signature + main: | + from pfun.maybe import maybe + + @maybe + def f(a: int, b: int) -> int: + pass + + reveal_type(f) # N: Revealed type is 'def (a: builtins.int, b: builtins.int) -> Union[pfun.maybe.Nothing, pfun.maybe.Just[builtins.int]]' +- case: variadic_decorators_preserve_trinary_signature + main: | + from pfun.maybe import maybe + + @maybe + def f(a: int, b: int, c: int) -> int: + pass + + reveal_type(f) # N: Revealed type is 'def (a: builtins.int, b: builtins.int, c: builtins.int) -> Union[pfun.maybe.Nothing, pfun.maybe.Just[builtins.int]]' +- case: curry_nullary_function + main: | + from pfun import curry + + @curry + def f() -> int: + pass + + reveal_type(f) # N: Revealed type is 'def () -> builtins.int' +- case: curry_unary_function + main: | + from pfun import curry + + @curry + def f(a: int) -> int: + pass + + reveal_type(f) # N: Revealed type is 'Overload(def (a: builtins.int) -> builtins.int, def (a: builtins.int) -> builtins.int)' +- case: curry_binary_function + main: | + from pfun import curry + + @curry + def f(a: int, b: int) -> int: + pass + + reveal_type(f) # N: Revealed type is 'Overload(def (a: builtins.int) -> def (b: builtins.int) -> builtins.int, def (a: builtins.int, b: builtins.int) -> builtins.int)' +- case: curry_trinary_function + main: | + from pfun import curry + + @curry + def f(a: int, b: int, c: int) -> int: + pass + + reveal_type(f) # N: Revealed type is 'Overload(def (a: builtins.int) -> def (b: builtins.int) -> def (c: builtins.int) -> builtins.int, def (a: builtins.int, b: builtins.int, c: builtins.int) -> builtins.int)' +- case: compose_simple_functions + main: | + from pfun import compose + + def f(a: str) -> int: + pass + + def g(a: int) -> float: + pass + + reveal_type(compose(g, f)) # N: Revealed type is 'def (builtins.str*) -> builtins.float*' +- case: immutable_has_correct_constructor + main: | + from pfun import Immutable + + class C(Immutable): + field: int + + C() # E: Too few arguments for "C" +- case: cant_set_immutable_attribute + main: | + from pfun import Immutable + + class C(Immutable): + field: int + + c = C(1) + c.field = 2 # E: Property "field" defined in "C" is read-only + + class D(C): + other_field: str + + d = D(1, '') + d.field = 2 # E: Property "field" defined in "D" is read-only +- case: effect_and_then_combines_protocols + main: | + from typing_extensions import Protocol + from typing import NoReturn + from pfun.effect import Effect + + + class P1(Protocol): + p1_attr: str + + class P2(Protocol): + p2_attr: int + + e1: Effect[P1, NoReturn, str] + e2: Effect[P2, NoReturn, int] + reveal_type(e1.and_then(lambda _: e2)) # N: Revealed type is 'pfun.effect.effect.Effect[pfun.effect.Intersection[main.P1, main.P2], , builtins.int*]' +- case: effect_and_then_does_not_combine_non_protocols + main: | + from typing import NoReturn + from pfun.effect import Effect + + e1: Effect[int, NoReturn, str] + e2: Effect[str, NoReturn, int] + + reveal_type(e1.and_then(lambda _: e2)) # N: Revealed type is 'pfun.effect.effect.Effect[Any, , builtins.int*]' +- case: effect_recover_combines_protocols + main: | + from typing_extensions import Protocol + from typing import NoReturn + from pfun.effect import Effect + + + class P1(Protocol): + p1_attr: str + + class P2(Protocol): + p2_attr: int + + e1: Effect[P1, ValueError, str] + e2: Effect[P2, NoReturn, str] + reveal_type(e1.recover(lambda _: e2)) # N: Revealed type is 'pfun.effect.effect.Effect[pfun.effect.Intersection[main.P1, main.P2], , builtins.str]' +- case: effect_recover_does_not_combine_non_protocols + main: | + from typing import NoReturn + from pfun.effect import Effect + + e1: Effect[int, ValueError, str] + e2: Effect[str, NoReturn, str] + + reveal_type(e1.recover(lambda _: e2)) # N: Revealed type is 'pfun.effect.effect.Effect[Any, , builtins.str]' +- case: get_environment_infers_type + main: | + from typing import NoReturn + from pfun.effect import Effect, get_environment + + def f() -> Effect[int, NoReturn, str]: + return get_environment().map(lambda env: env.upper()) # E: "int" has no attribute "upper" +- case: effect_combine_checks_args + main: | + from typing import Any, NoReturn + from pfun.effect import success, combine, Effect + + def f(a: int, b: int) -> str: + pass + + combine(success(''), success(''))(f) # E: Argument 1 has incompatible type "Callable[[int, int], str]"; expected "Callable[[str, str], str]" +- case: effect_combine_combines_protocols + main: | + from typing_extensions import Protocol + from typing import NoReturn + from pfun.effect import Effect, combine + + + class P1(Protocol): + p1_attr: str + + class P2(Protocol): + p2_attr: int + + e1: Effect[P1, NoReturn, str] + e2: Effect[P2, NoReturn, str] + + def f(a: str, b: str) -> str: + pass + + reveal_type(combine(e1, e2)(f)) # N: Revealed type is 'pfun.effect.effect.Effect[pfun.effect.Intersection[main.P1, main.P2], , builtins.str*]' +- case: effect_combine_unions_errors + main: | + from typing import Any + from pfun.effect import Effect, combine + + e1: Effect[Any, ValueError, str] + e2: Effect[Any, IOError, str] + + def f(a: str, b: str) -> str: + pass + + reveal_type(combine(e1, e2)(f)) # N: Revealed type is 'pfun.effect.effect.Effect[Any, Union[builtins.ValueError, builtins.OSError], builtins.str*]' diff --git a/tests/test_reader.py b/tests/test_reader.py index b2e6cbd..19eaa23 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,7 +1,9 @@ +from hypothesis import assume, given + +from pfun import compose, identity, reader + from .monad_test import MonadTest -from .strategies import anything, unaries, readers -from hypothesis import given, assume -from pfun import reader, identity, compose +from .strategies import anything, readers, unaries from .utils import recursion_limit diff --git a/tests/test_result.py b/tests/test_result.py index 9777b6a..ee257ba 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -1,4 +1,4 @@ -from pfun.result import result, Ok, Error +from pfun.result import Error, Ok, result def test_result_decorator(): diff --git a/tests/test_state.py b/tests/test_state.py index 427c764..faee22b 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,8 +1,9 @@ -from hypothesis import given, assume +from hypothesis import assume, given +from pfun import compose, identity, state from tests.monad_test import MonadTest -from pfun import state, identity, compose -from tests.strategies import anything, unaries, states +from tests.strategies import anything, states, unaries + from .utils import recursion_limit diff --git a/tests/test_trampoline.py b/tests/test_trampoline.py index ee1327c..ab050d8 100644 --- a/tests/test_trampoline.py +++ b/tests/test_trampoline.py @@ -1,9 +1,10 @@ -from hypothesis import given, assume -from pfun.trampoline import Done, with_effect, sequence, filter_m, map_m -from pfun import identity, compose +from hypothesis import assume, given + +from pfun import compose, identity +from pfun.trampoline import Done, filter_m, map_m, sequence, with_effect -from .strategies import trampolines, unaries, anything from .monad_test import MonadTest +from .strategies import anything, trampolines, unaries from .utils import recursion_limit diff --git a/tests/test_types.py b/tests/test_types.py deleted file mode 100644 index 9c73043..0000000 --- a/tests/test_types.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import pytest - -from mypy import api as mypy_api - -parametrize = pytest.mark.parametrize - - -def python_files(path): - py_files = [] - current_folder, _ = os.path.split(__file__) - folder = os.path.join(current_folder, path) - for root, _, files in os.walk(folder): - for file in files: - if file == '__init__.py': - continue - if file.endswith('.py'): - py_files.append(os.path.join(root, file)) - return py_files - - -def type_check(file): - return mypy_api.run(['--config-file=tests/mypy.ini', file]) - - -@parametrize('file', python_files('type_tests/positives')) -def test_positives(file): - normal_report, error_report, exit_code = type_check(file) - if error_report or exit_code != 0: - pytest.fail(error_report) - - -@parametrize('file', python_files('type_tests/negatives')) -def test_negatives(file): - normal_report, error_report, exit_code = type_check(file) - if not normal_report or exit_code == 0: - pytest.fail('No type error emitted for {}'.format(file)) diff --git a/tests/test_util.py b/tests/test_util.py index 7391276..c4938b7 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,7 +1,7 @@ from hypothesis import given from pfun import util -from tests.strategies import anything, unaries, lists, dicts +from tests.strategies import anything, dicts, lists, unaries @given(anything(allow_nan=False)) diff --git a/tests/test_writer.py b/tests/test_writer.py index e58d348..1116f64 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -1,9 +1,9 @@ -from hypothesis import given, assume +from hypothesis import assume, given -from pfun import writer -from pfun import identity, compose +from pfun import compose, identity, writer from tests.monad_test import MonadTest -from tests.strategies import anything, unaries, writers, monoids +from tests.strategies import anything, monoids, unaries, writers + from .utils import recursion_limit diff --git a/tests/type_tests/__init__.py b/tests/type_tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/type_tests/negatives/__init__.py b/tests/type_tests/negatives/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/type_tests/negatives/compose_non_composable_arguments.py b/tests/type_tests/negatives/compose_non_composable_arguments.py deleted file mode 100644 index 12143b4..0000000 --- a/tests/type_tests/negatives/compose_non_composable_arguments.py +++ /dev/null @@ -1,12 +0,0 @@ -from pfun import compose - - -def f(a: int) -> int: - pass - - -def g(a: int) -> str: - pass - - -compose(f, g) diff --git a/tests/type_tests/negatives/compose_non_unary_function.py b/tests/type_tests/negatives/compose_non_unary_function.py deleted file mode 100644 index e6c966c..0000000 --- a/tests/type_tests/negatives/compose_non_unary_function.py +++ /dev/null @@ -1,12 +0,0 @@ -from pfun import compose - - -def f(a: int, b: int) -> int: - pass - - -def g(a: int) -> int: - pass - - -compose(f, g) diff --git a/tests/type_tests/negatives/compose_return_type.py b/tests/type_tests/negatives/compose_return_type.py deleted file mode 100644 index 9e5a4d0..0000000 --- a/tests/type_tests/negatives/compose_return_type.py +++ /dev/null @@ -1,14 +0,0 @@ -from pfun import compose - - -def f(a: int) -> str: - pass - - -def g(a: str) -> int: - pass - - -h = compose(f, g) - -map(h, range(5)) diff --git a/tests/type_tests/negatives/curry_arguments.py b/tests/type_tests/negatives/curry_arguments.py deleted file mode 100644 index 39f5385..0000000 --- a/tests/type_tests/negatives/curry_arguments.py +++ /dev/null @@ -1,9 +0,0 @@ -from pfun import curry - - -@curry -def f(a: int, b: int) -> str: - pass - - -f(1)('') diff --git a/tests/type_tests/negatives/curry_callable_instance_arguments.py b/tests/type_tests/negatives/curry_callable_instance_arguments.py deleted file mode 100644 index 2ffe08b..0000000 --- a/tests/type_tests/negatives/curry_callable_instance_arguments.py +++ /dev/null @@ -1,11 +0,0 @@ -from pfun import curry - - -class C: - def __call__(self, a: int, b: int) -> int: - pass - - -c = curry(C()) - -c('')('') + 1 diff --git a/tests/type_tests/negatives/curry_generic_arguments.py b/tests/type_tests/negatives/curry_generic_arguments.py deleted file mode 100644 index 257c3ab..0000000 --- a/tests/type_tests/negatives/curry_generic_arguments.py +++ /dev/null @@ -1,12 +0,0 @@ -from pfun import curry -import typing as t - -A = t.TypeVar('A') - - -def f(a: A, b: A) -> A: - pass - - -g = curry(f)('') -map(g, range(10)) diff --git a/tests/type_tests/negatives/curry_return_type.py b/tests/type_tests/negatives/curry_return_type.py deleted file mode 100644 index 6415a2b..0000000 --- a/tests/type_tests/negatives/curry_return_type.py +++ /dev/null @@ -1,9 +0,0 @@ -from pfun import curry - - -@curry -def f(a: int, b: str) -> str: - pass - - -map(f(1), range(5)) diff --git a/tests/type_tests/negatives/immutable_arguments.py b/tests/type_tests/negatives/immutable_arguments.py deleted file mode 100644 index a00dd6e..0000000 --- a/tests/type_tests/negatives/immutable_arguments.py +++ /dev/null @@ -1,8 +0,0 @@ -from pfun import Immutable - - -class C(Immutable): - a: int - - -C('') diff --git a/tests/type_tests/negatives/immutable_assignment.py b/tests/type_tests/negatives/immutable_assignment.py deleted file mode 100644 index 5d6ac65..0000000 --- a/tests/type_tests/negatives/immutable_assignment.py +++ /dev/null @@ -1,9 +0,0 @@ -from pfun import Immutable - - -class C(Immutable): - a: int - - -c = C(1) -c.a = 1 diff --git a/tests/type_tests/negatives/list_bind_arguments.py b/tests/type_tests/negatives/list_bind_arguments.py deleted file mode 100644 index 14459e3..0000000 --- a/tests/type_tests/negatives/list_bind_arguments.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List(['1', '2', '3']).and_then(lambda x: List([x**2])) diff --git a/tests/type_tests/negatives/list_bind_return_type.py b/tests/type_tests/negatives/list_bind_return_type.py deleted file mode 100644 index 1b364a6..0000000 --- a/tests/type_tests/negatives/list_bind_return_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[str]: - return List(i for i in (1, 2, 3)).and_then(lambda x: List([x**2])) diff --git a/tests/type_tests/negatives/list_map_arguments.py b/tests/type_tests/negatives/list_map_arguments.py deleted file mode 100644 index 8209863..0000000 --- a/tests/type_tests/negatives/list_map_arguments.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List([1]).map(str).map(lambda x: x**2) diff --git a/tests/type_tests/negatives/list_map_return_type.py b/tests/type_tests/negatives/list_map_return_type.py deleted file mode 100644 index 9b08c6e..0000000 --- a/tests/type_tests/negatives/list_map_return_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[str]: - return List(i for i in (1, 2, 3)).map(lambda x: x**2) diff --git a/tests/type_tests/negatives/list_pure.py b/tests/type_tests/negatives/list_pure.py deleted file mode 100644 index 0c11397..0000000 --- a/tests/type_tests/negatives/list_pure.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List(['']) diff --git a/tests/type_tests/negatives/maybe_bind_arguments.py b/tests/type_tests/negatives/maybe_bind_arguments.py deleted file mode 100644 index 658551c..0000000 --- a/tests/type_tests/negatives/maybe_bind_arguments.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import Maybe, Just - - -def test_just() -> Maybe[str]: - return Just(1).and_then(lambda a: str(a)) diff --git a/tests/type_tests/negatives/maybe_bind_return_type.py b/tests/type_tests/negatives/maybe_bind_return_type.py deleted file mode 100644 index 69d88a0..0000000 --- a/tests/type_tests/negatives/maybe_bind_return_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import Maybe, Just - - -def test_just() -> Maybe[int]: - return Just(1).and_then(lambda a: Just('')) diff --git a/tests/type_tests/negatives/maybe_map_arguments.py b/tests/type_tests/negatives/maybe_map_arguments.py deleted file mode 100644 index 5bd8aec..0000000 --- a/tests/type_tests/negatives/maybe_map_arguments.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import Maybe, Just - - -def test() -> Maybe[str]: - return Just(1).map(lambda a: a.lower()) diff --git a/tests/type_tests/negatives/maybe_map_return_type.py b/tests/type_tests/negatives/maybe_map_return_type.py deleted file mode 100644 index 49de9d2..0000000 --- a/tests/type_tests/negatives/maybe_map_return_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import Maybe, Just - - -def test_just() -> Maybe[int]: - return Just(1).map(str) diff --git a/tests/type_tests/negatives/maybe_pure.py b/tests/type_tests/negatives/maybe_pure.py deleted file mode 100644 index 3c3ee06..0000000 --- a/tests/type_tests/negatives/maybe_pure.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import Just, Maybe - - -def test() -> Maybe[int]: - return Just('test') diff --git a/tests/type_tests/positives/compose_arguments.py b/tests/type_tests/positives/compose_arguments.py deleted file mode 100644 index 698beb5..0000000 --- a/tests/type_tests/positives/compose_arguments.py +++ /dev/null @@ -1,16 +0,0 @@ -from pfun import compose - - -def f(a: int) -> int: - pass - - -def g(a: int) -> int: - pass - - -def h(a: int) -> int: - pass - - -compose(f, g, h) diff --git a/tests/type_tests/positives/compose_callable_instance_arguments.py b/tests/type_tests/positives/compose_callable_instance_arguments.py deleted file mode 100644 index 78c12fe..0000000 --- a/tests/type_tests/positives/compose_callable_instance_arguments.py +++ /dev/null @@ -1,11 +0,0 @@ -from pfun import curry - - -class C: - def __call__(self, a: int, b: int) -> int: - pass - - -c = curry(C()) - -c(1)(1) + 1 diff --git a/tests/type_tests/positives/compose_return_type.py b/tests/type_tests/positives/compose_return_type.py deleted file mode 100644 index 87f6609..0000000 --- a/tests/type_tests/positives/compose_return_type.py +++ /dev/null @@ -1,14 +0,0 @@ -from pfun import compose - - -def f(a: int) -> int: - pass - - -def g(a: int) -> int: - pass - - -h = compose(f, g) - -map(h, range(5)) diff --git a/tests/type_tests/positives/curry_arguments.py b/tests/type_tests/positives/curry_arguments.py deleted file mode 100644 index a0437dc..0000000 --- a/tests/type_tests/positives/curry_arguments.py +++ /dev/null @@ -1,9 +0,0 @@ -from pfun import curry - - -@curry -def f(a: int, b: int) -> str: - pass - - -f(1)(1) diff --git a/tests/type_tests/positives/curry_generic_argument.py b/tests/type_tests/positives/curry_generic_argument.py deleted file mode 100644 index d0b9d4f..0000000 --- a/tests/type_tests/positives/curry_generic_argument.py +++ /dev/null @@ -1,11 +0,0 @@ -from pfun import curry -import typing as t - -A = t.TypeVar('A') - - -def f(a: A, b: A) -> A: - pass - - -map(curry(f)(1), range(10)) diff --git a/tests/type_tests/positives/curry_return_type.py b/tests/type_tests/positives/curry_return_type.py deleted file mode 100644 index d4bf85e..0000000 --- a/tests/type_tests/positives/curry_return_type.py +++ /dev/null @@ -1,9 +0,0 @@ -from pfun import curry - - -@curry -def f(a: int, b: int) -> str: - pass - - -map(f(1), range(5)) diff --git a/tests/type_tests/positives/immutable_arguments.py b/tests/type_tests/positives/immutable_arguments.py deleted file mode 100644 index bfb1878..0000000 --- a/tests/type_tests/positives/immutable_arguments.py +++ /dev/null @@ -1,8 +0,0 @@ -from pfun import Immutable - - -class C(Immutable): # type: ignore - a: int - - -C(1) diff --git a/tests/type_tests/positives/list_bind_arguments.py b/tests/type_tests/positives/list_bind_arguments.py deleted file mode 100644 index dac9ea3..0000000 --- a/tests/type_tests/positives/list_bind_arguments.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List(i for i in (1, 2, 3)).and_then(lambda x: List([x**2])) diff --git a/tests/type_tests/positives/list_bind_return_type.py b/tests/type_tests/positives/list_bind_return_type.py deleted file mode 100644 index dac9ea3..0000000 --- a/tests/type_tests/positives/list_bind_return_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List(i for i in (1, 2, 3)).and_then(lambda x: List([x**2])) diff --git a/tests/type_tests/positives/list_map_arguments.py b/tests/type_tests/positives/list_map_arguments.py deleted file mode 100644 index 5e2a0a3..0000000 --- a/tests/type_tests/positives/list_map_arguments.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List(i for i in (1, 2, 3)).map(lambda x: x**2) diff --git a/tests/type_tests/positives/list_map_return_type.py b/tests/type_tests/positives/list_map_return_type.py deleted file mode 100644 index 5e2a0a3..0000000 --- a/tests/type_tests/positives/list_map_return_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List(i for i in (1, 2, 3)).map(lambda x: x**2) diff --git a/tests/type_tests/positives/list_pure.py b/tests/type_tests/positives/list_pure.py deleted file mode 100644 index 0024cd5..0000000 --- a/tests/type_tests/positives/list_pure.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun import List - - -def test() -> List[int]: - return List([1]) diff --git a/tests/type_tests/positives/maybe_bind_arguments.py b/tests/type_tests/positives/maybe_bind_arguments.py deleted file mode 100644 index fefbb39..0000000 --- a/tests/type_tests/positives/maybe_bind_arguments.py +++ /dev/null @@ -1,10 +0,0 @@ -from pfun.maybe import Maybe, Just - - -def test_just() -> Maybe[str]: - return Just(1).and_then(lambda a: Just(str(a))) - - -# TODO -# def test_nothing() -> Maybe[str]: -# return Nothing().and_then(lambda a: Just('')) diff --git a/tests/type_tests/positives/maybe_bind_return_type.py b/tests/type_tests/positives/maybe_bind_return_type.py deleted file mode 100644 index 13177c4..0000000 --- a/tests/type_tests/positives/maybe_bind_return_type.py +++ /dev/null @@ -1,9 +0,0 @@ -from pfun.maybe import Maybe, Nothing, Just - - -def test_just() -> Maybe[int]: - return Just(1).and_then(lambda a: Just(1)) - - -def test_nothing() -> Maybe[int]: - return Just(1).and_then(lambda a: Nothing()) diff --git a/tests/type_tests/positives/maybe_map_arguments.py b/tests/type_tests/positives/maybe_map_arguments.py deleted file mode 100644 index f99df29..0000000 --- a/tests/type_tests/positives/maybe_map_arguments.py +++ /dev/null @@ -1,9 +0,0 @@ -from pfun.maybe import Maybe, Nothing, Just - - -def test_just() -> Maybe[str]: - return Just('test').map(lambda a: a.lower()) - - -def test_nothing() -> Maybe[str]: - return Nothing().map(lambda a: a.lower()) diff --git a/tests/type_tests/positives/maybe_map_return_type.py b/tests/type_tests/positives/maybe_map_return_type.py deleted file mode 100644 index db87895..0000000 --- a/tests/type_tests/positives/maybe_map_return_type.py +++ /dev/null @@ -1,12 +0,0 @@ -from pfun.maybe import Maybe, Nothing, Just -from pfun import identity - -from typing import Any - - -def test_just() -> Maybe[int]: - return Just(1).map(lambda a: a * 2) - - -def test_nothing() -> Maybe[Any]: - return Nothing().map(identity) diff --git a/tests/type_tests/positives/maybe_pure.py b/tests/type_tests/positives/maybe_pure.py deleted file mode 100644 index b7480f3..0000000 --- a/tests/type_tests/positives/maybe_pure.py +++ /dev/null @@ -1,5 +0,0 @@ -from pfun.maybe import Just, Maybe - - -def test() -> Maybe[int]: - return Just(1) diff --git a/tests/utils.py b/tests/utils.py index aedc43b..2b653af 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@ -from contextlib import contextmanager import sys +from contextlib import contextmanager @contextmanager diff --git a/tox.ini b/tox.ini index 5890eba..b49c2a4 100644 --- a/tox.ini +++ b/tox.ini @@ -7,4 +7,4 @@ whitelist_externals = poetry commands = poetry build poetry install - poetry run pytest -n 4 -p no:sugar + poetry run pytest -rsx -p no:sugar --mypy-ini-file=setup.cfg