diff --git a/.coveragerc b/.coveragerc index ed33143..188cbaf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,8 @@ [run] source = result +omit = + result/typetests.py [report] # Regexes for lines to exclude from consideration diff --git a/.travis.yml b/.travis.yml index 2a7e8eb..d80972c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,12 @@ install: script: - pip install -e . - py.test - - mypy result/result.py + - if [ $(python -c 'import sys; print(sys.version_info.minor)') -gt 4 ]; then + mypy result/result.py; + mypy result/typetests.py; + echo "Mypy tests done"; + else + echo "Skipping mypy"; + fi after_success: - coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5bf97..10378c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ Possible log types: - `[fixed]` for any bug fixes. - `[security]` to invite users to upgrade in case of vulnerabilities. +## Unreleased + + - [changed] Type annotations: Change parameter order + from `Result[E, T]` to `Result[T, E]` to match Rust/OCaml/F# (#7) + ## [0.4.1] - 2020-02-17 - [added] Add `py.typed` for PEP561 package compliance (#16) diff --git a/README.rst b/README.rst index f88a9b5..d5a7648 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,8 @@ Result :alt: Coverage :target: https://coveralls.io/github/dbrgn/result -A simple Result type for Python 3 `inspired by Rust `__. +A simple Result type for Python 3 `inspired by Rust +`__, fully type annotated. The idea is that a ``Result`` value can be either ``Ok(value)`` or ``Err(error)``, with a way to differentiate between the two. It will change code like this: @@ -62,8 +63,8 @@ side, you don't have to return semantically unclear tuples anymore. Not all methods (https://doc.rust-lang.org/std/result/enum.Result.html) have been implemented, only the ones that make sense in the Python context. You still -don't get any type safety, but some easier handling of types that can be OK or -not, without resorting to custom exceptions. +don't get any type safety at runtime, but some easier handling of types that can +be OK or not, without resorting to custom exceptions. API diff --git a/requirements-dev.txt b/requirements-dev.txt index 8598ef4..f023f7b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ pytest==2.8.5 pytest-cov==2.2.0 pytest-pep8==1.0.6 --e git+https://github.com/python/mypy@005fd6b357b995f13e3d90293cabf3c8c8c2a952#egg=mypy +mypy==0.720 diff --git a/result/result.py b/result/result.py index 2490609..1c7983b 100644 --- a/result/result.py +++ b/result/result.py @@ -1,29 +1,33 @@ from typing import Callable, Generic, TypeVar, Union, Any, Optional, cast, overload -E = TypeVar("E") +T = TypeVar("T") # Success type +E = TypeVar("E") # Error type F = TypeVar("F") -T = TypeVar("T") U = TypeVar("U") -class Result(Generic[E, T]): +class Result(Generic[T, E]): """ A simple `Result` type inspired by Rust. Not all methods (https://doc.rust-lang.org/std/result/enum.Result.html) have been implemented, only the ones that make sense in the Python context. """ - def __init__(self, is_ok: bool, value: Union[E, T], force: bool = False) -> None: + def __init__(self, is_ok: bool, value: Union[T, E], force: bool = False) -> None: """Do not call this constructor, use the Ok or Err class methods instead. There are no type guarantees on the value if this is called directly. Args: - is_ok: If this represents an ok result - value: The value inside the result - force: Force creation of the object. This is false by default to prevent + is_ok: + If this represents an ok result + value: + The value inside the result + force: + Force creation of the object. This is false by default to prevent accidentally creating instance of a Result in an unsafe way. + """ if force is not True: raise RuntimeError("Don't instantiate a Result directly. " @@ -51,20 +55,20 @@ def __repr__(self) -> str: @classmethod @overload - def Ok(cls) -> 'Result[E, bool]': + def Ok(cls) -> 'Result[bool, Any]': pass @classmethod @overload - def Ok(cls, value: T) -> 'Result[E, T]': + def Ok(cls, value: T) -> 'Result[T, Any]': pass @classmethod - def Ok(cls, value: Any = True) -> 'Result[E, Any]': + def Ok(cls, value: Any = True) -> 'Result[Any, Any]': return cls(is_ok=True, value=value, force=True) @classmethod - def Err(cls, error: E) -> 'Result[E, T]': + def Err(cls, error: E) -> 'Result[Any, E]': return cls(is_ok=False, value=error, force=True) def is_ok(self) -> bool: @@ -87,7 +91,7 @@ def err(self) -> Optional[E]: return cast(E, self._value) if self.is_err() else None @property - def value(self) -> Union[E, T]: + def value(self) -> Union[T, E]: """ Return the inner value. This might be either the ok or the error type. """ @@ -120,14 +124,14 @@ def unwrap_or(self, default: T) -> T: else: return default - def map(self, op: Callable[[T], U]) -> 'Result[E, U]': + def map(self, op: Callable[[T], U]) -> 'Result[U, E]': """ If contained result is `Ok`, return `Ok` with original value mapped to a new value using the passed in function. Otherwise return `Err` with same value. """ if not self._is_ok: - return cast(Result[E, U], self) + return cast(Result[U, E], self) return Ok(op(cast(T, self._value))) def map_or(self, default: U, op: Callable[[T], U]) -> U: @@ -153,35 +157,35 @@ def map_or_else( return default_op() return op(cast(T, self._value)) - def map_err(self, op: Callable[[E], F]) -> 'Result[F, T]': + def map_err(self, op: Callable[[E], F]) -> 'Result[T, F]': """ If contained result is `Err`, return `Err` with original value mapped to a new value using the passed in `op` function. Otherwise return `Ok` with the same value. """ if self._is_ok: - return cast(Result[F, T], self) + return cast(Result[T, F], self) return Err(op(cast(E, self._value))) @overload -def Ok() -> Result[E, bool]: +def Ok() -> Result[bool, Any]: pass @overload -def Ok(value: T) -> Result[E, T]: +def Ok(value: T) -> Result[T, Any]: pass -def Ok(value: Any = True) -> Result[E, Any]: +def Ok(value: Any = True) -> Result[Any, Any]: """ Shortcut function to create a new Result. """ return Result.Ok(value) -def Err(error: E) -> Result[E, T]: +def Err(error: E) -> Result[Any, E]: """ Shortcut function to create a new Result. """ diff --git a/result/typetests.py b/result/typetests.py new file mode 100644 index 0000000..ba0ee86 --- /dev/null +++ b/result/typetests.py @@ -0,0 +1,20 @@ +from typing import List, Optional + +from .result import Result, Ok, Err + + +res1 = Result.Ok('hello') # type: Result[str, int] +if res1.is_ok(): + ok = res1.ok() # type: Optional[str] + mapped_to_float = res1.map_or(1.0, lambda s: len(s) * 1.5) # type: float +else: + err = res1.err() # type: Optional[int] + mapped_to_list = res1.map_err(lambda e: [e]).err() # type: Optional[List[int]] + +# Test constructor functions +res2 = Ok() +res3 = Result.Ok() +res4 = Ok(42) +res5 = Result.Ok(23) +res6 = Err(1) +res7 = Result.Err(2)