Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

Commit

Permalink
Improve README.md executability unit-test
Browse files Browse the repository at this point in the history
* Test extracts code blocks and checks them for executability
* Small changes to README to allow proper code execution
  - README.md: Renamed classes that redefined previous class
* Removed now obsolete readme_examples.py
  • Loading branch information
wchresta committed Jan 7, 2020
1 parent 2cca05f commit 9e84f29
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 151 deletions.
79 changes: 42 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ For example, we could use the `Either` ADT from above to implement a sort of err

```python
# Defined in some other module, perhaps
def some_operation() -> Either[Exception, int]
def some_operation() -> Either[Exception, int]:
return Either.RIGHT(22) # Example of building a constructor

# Run some_operation, and handle the success or failure
default_value = 5
return some_operation().match(
unpacked_result = some_operation().match(
# In this case, we're going to ignore any exception we receive
left=lambda ex: default_value,
right=lambda result: result)
Expand All @@ -77,14 +78,14 @@ _Aside: this is very similar to how error handling is implemented in languages l
One can do the same thing with the `Expression` type above (just more cases to match):

```python
e: Expression
result = e.match(
literal=lambda n: ...,
unary_minus=lambda expr: ...,
add=lambda lhs, rhs: ...,
minus=lambda lhs, rhs: ...,
multiply=lambda lhs, rhs: ...,
divide=lambda lhs, rhs: ...)
def handle_expression(e: Expression):
return e.match(
literal=lambda n: ...,
unary_minus=lambda expr: ...,
add=lambda lhs, rhs: ...,
minus=lambda lhs, rhs: ...,
multiply=lambda lhs, rhs: ...,
divide=lambda lhs, rhs: ...)
```

## Compared to Enums
Expand All @@ -94,7 +95,8 @@ ADTs are somewhat similar to [`Enum`s](https://docs.python.org/3/library/enum.ht
For example, an `Enum` version of `Expression` might look like:

```python
class Expression(Enum):
from enum import Enum, auto
class EnumExpression(Enum):
LITERAL = auto()
UNARY_MINUS = auto()
ADD = auto()
Expand All @@ -114,51 +116,52 @@ Algebraic data types are a relatively recent introduction to object-oriented pro
Continuing our examples with the `Expression` ADT, here's how one might represent it with inheritance in Python:

```python
class Expression(ABC):
from abc import ABC
class ABCExpression(ABC):
pass

class LiteralExpression(Expression):
class LiteralExpression(ABCExpression):
def __init__(self, value: float):
pass

class UnaryMinusExpression(Expression):
def __init__(self, inner: Expression):
class UnaryMinusExpression(ABCExpression):
def __init__(self, inner: ABCExpression):
pass

class AddExpression(Expression):
def __init__(self, lhs: Expression, rhs: Expression):
class AddExpression(ABCExpression):
def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
pass

class MinusExpression(Expression):
def __init__(self, lhs: Expression, rhs: Expression):
class MinusExpression(ABCExpression):
def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
pass

class MultiplyExpression(Expression):
def __init__(self, lhs: Expression, rhs: Expression):
class MultiplyExpression(ABCExpression):
def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
pass

class DivideExpression(Expression):
def __init__(self, lhs: Expression, rhs: Expression):
class DivideExpression(ABCExpression):
def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
pass
```

This is noticeably more verbose, and the code to consume these types gets much more complex as well:

```python
e: Expression
e: ABCExpression = UnaryMinusExpression(LiteralExpression(3)) # Example of creating an expression

if isinstance(e, LiteralExpression):
result = # do something with e.value
result = ... # do something with e.value
elif isinstance(e, UnaryMinusExpression):
result = # do something with e.inner
result = ... # do something with e.inner
elif isinstance(e, AddExpression):
result = # do something with e.lhs and e.rhs
result = ... # do something with e.lhs and e.rhs
elif isinstance(e, MinusExpression):
result = # do something with e.lhs and e.rhs
result = ... # do something with e.lhs and e.rhs
elif isinstance(e, MultiplyExpression):
result = # do something with e.lhs and e.rhs
result = ... # do something with e.lhs and e.rhs
elif isinstance(e, DivideExpression):
result = # do something with e.lhs and e.rhs
result = ... # do something with e.lhs and e.rhs
else:
raise ValueError(f'Unexpected type of expression: {e}')
```
Expand Down Expand Up @@ -242,6 +245,7 @@ plugins = adt.mypy_plugin

To begin defining your own data type, import the `@adt` decorator and `Case[…]` annotation:

[//]: # (README_TEST:AT_TOP)
```python
from adt import adt, Case
```
Expand All @@ -250,15 +254,15 @@ Then, define a new Python class, upon which you apply the `@adt` decorator:

```python
@adt
class MyADT:
class MyADT1:
pass
```

For each case (variant) that your ADT will have, declare a field with the `Case` annotation. It's conventional to declare your fields with ALL_UPPERCASE names, but the only true restriction is that they _cannot_ be lowercase.

```python
@adt
class MyADT:
class MyADT2:
FIRST_CASE: Case
SECOND_CASE: Case
```
Expand All @@ -267,7 +271,7 @@ If you want to associate some data with a particular case, list the type of that

```python
@adt
class MyADT:
class MyADT3:
FIRST_CASE: Case
SECOND_CASE: Case
STRING_CASE: Case[str]
Expand All @@ -277,7 +281,7 @@ You can build cases with arbitrarily many associated pieces of data, as long as

```python
@adt
class MyADT:
class MyADT4:
FIRST_CASE: Case
SECOND_CASE: Case
STRING_CASE: Case[str]
Expand Down Expand Up @@ -305,7 +309,7 @@ Given an ADT defined as follows:

```python
@adt
class ExampleADT:
class MyADT5:
EMPTY: Case
INTEGER: Case[int]
STRING_PAIR: Case[str, str]
Expand All @@ -318,16 +322,17 @@ The `@adt` decorator will automatically generate accessor methods of the followi
return None

def integer(self) -> int:
# unpacks int value and returns it
... # unpacks int value and returns it

def string_pair(self) -> Tuple[str, str]:
# unpacks strings and returns them in a tuple
... # unpacks strings and returns them in a tuple
```

These accessors can be used to obtain the data associated with the ADT case, but **accessors will throw an exception if the ADT was not constructed with the matching case**. This is a shorthand when you already know the case of an ADT object.

`@adt` will also automatically generate a pattern-matching method, which can be used when you _don't_ know which case you have ahead of time:

[//]: # (README_TEST:IGNORE)
```python
Result = TypeVar('Result')

Expand Down
3 changes: 3 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@

def invalidPatternMatch(*args: Any) -> NoReturn:
assert False, 'Pattern matching failed'


PATH_TO_TEST_BASE_DIRECTORY = os.path.dirname(__file__)
107 changes: 0 additions & 107 deletions tests/source_files/readme_examples.py

This file was deleted.

25 changes: 20 additions & 5 deletions tests/test_mypy_plugin.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import os
import sys
import unittest

import mypy.main
import mypy.version
import os
import sys

from tests import helpers
from tests.test_readme import extract_code_from_readme


class TestMyPyPlugin(unittest.TestCase):
def test_issue21(self) -> None:
self._call_mypy_on_source_file("issue21.py")

@unittest.expectedFailure # Issue #26 is still unfixed
@unittest.expectedFailure # Issue #25 is still unfixed
def test_issue25(self) -> None:
# Activate it when working on it.
# cf. https://github.com/jspahrsummers/adt/issues/25
Expand All @@ -23,14 +27,25 @@ def test_issue26(self) -> None:

@unittest.expectedFailure # Fails because issue #26 is still unfixed
def test_readme_examples(self) -> None:
self._call_mypy_on_source_file("readme_examples.py")
readme_code = extract_code_from_readme()

try:
mypy.main.main(script_path=None,
stdout=sys.stdout,
stderr=sys.stderr,
args=[
"-c",
readme_code,
])
except SystemExit:
self.fail(msg="Error during type-check of readme-code")

def _call_mypy_on_source_file(self, source_file_name: str) -> None:
print(
f"Testing {source_file_name} with mypy=={mypy.version.__version__}"
)
self._call_mypy_on_path(
os.path.join(os.path.dirname(__file__), "source_files",
os.path.join(helpers.PATH_TO_TEST_BASE_DIRECTORY, "source_files",
source_file_name))

def _call_mypy_on_path(self, testfile: str) -> None:
Expand Down
Loading

0 comments on commit 9e84f29

Please sign in to comment.