Skip to content

Commit

Permalink
Update codealong
Browse files Browse the repository at this point in the history
  • Loading branch information
ImAKappa committed Jun 16, 2024
1 parent e24474b commit 58f8157
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 102 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ View the tutorial at [https://imakappa.github.io/pftp/](https://imakappa.github.
- `ideas` Notes on ideas for the tutorial/upgrades to core library
- `pftp` core library for automation and writing utilities
- `teaching` Notes on teaching
- `test` automated testing
- `test` automated testing

## TODO

- [ ] Make navbar appear up when scrolling up
11 changes: 7 additions & 4 deletions codealongs/fortune.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""
from typing import Self
from pathlib import Path
from pftp.codealong import CodeAlong, CodeAlongWriter
from pftp.codealong import CodeAlong, CodeAlongWriter, CodeAlongTester

class FortuneTeller(CodeAlong):

Expand Down Expand Up @@ -103,7 +103,10 @@ def fortune_3(self) -> None:
print()

if __name__ == "__main__":
print("Decider")
writer = CodeAlongWriter(FortuneTeller(), indent_amount=2)

tester = CodeAlongTester()
tester.test_for_errors(FortuneTeller())

writer = CodeAlongWriter()
output = Path("./docs/1-Fortune-Telling/fortune")
writer.write(output)
writer.write(output, FortuneTeller())
30 changes: 23 additions & 7 deletions docs/1-Fortune-Telling/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@ the Pythoness[^1] - the great Oracle of Delphi - by reciting a few lines of the
!!! note "It's normal to be confused. In fact, it's all part of the plan."

The code below is purposely NOT explained in detail.
The goal here is simply to get exposed to writing Python code.
Before understanding the why and how, we first need to develop some baseline familiarity with typing Python and running code in Thonny.
There are two reasons:

1. **To develop familiarity with Python** -
Before understanding the why, we first need to develop some baseline familiarity with typing Python and running code in Thonny.
2. **To get comfy with being confused** - Being confused about novel code is completely normal for even experienced programmers, and it's good to practice getting used to that. In general, being able to sit with the discomfort of confusion is a useful skill when learning.

!!! danger "Type, don't copy"

Manually type out the all the code below into the Thonny editor.
You're **wasting your time** if you just copy and paste.

Pythia sees all, knows all, but has a fairly limited list of possible responses.
She's a bit picky and expects questions with "yes" or "no" answers.

```python title="fortune.py"
--8<-- "1-Fortune-Telling/fortune/fortune_0.py"
Expand All @@ -44,6 +48,11 @@ We should be able to ask her a question, first.
--8<-- "1-Fortune-Telling/fortune/fortune_1.py"
```

!!! question

What happens when you remove the `\n` character?
Re-run the script and compare the difference.

That's better. But it's weird for her to respond if we don't ask a question.

![Example usage of fortune_1.py script in Thonny](./01-fortune_1_thonny-2024-06-15.png)
Expand All @@ -54,6 +63,11 @@ Pythia should double check that we've asked her a question.
--8<-- "1-Fortune-Telling/fortune/fortune_2.py"
```

!!! tip

Pressing the `enter` key will submit your question to Pythia.
Make sure to type your question first.

Finally, Pythia is a patient seer, and will continue to answer our questions until we are satisified.

```python title="fortune.py"
Expand All @@ -64,9 +78,11 @@ Finally, Pythia is a patient seer, and will continue to answer our questions unt

## 🪞 Reflection

> Section in progress ✏️. Come back soon.
!!! note "Order of execution"
Take the time to complete the following reflection questions.

As always, Python code is executed from top to bottom, one line at a time
so the order of our instructions really matters!
1. Modify the code so that Pythia asks "What do you desire to know?"
2. Make a list of everything that confused you. Here are some sample prompts to get you started:
1. Why did we write _____?
2. What does _____ do?
3. How does _____ work?
3.
18 changes: 0 additions & 18 deletions docs/javascripts/mathjax.js

This file was deleted.

77 changes: 47 additions & 30 deletions pftp/codealong.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
from pprint import pprint
from io import StringIO
from pathlib import Path
import re
import textwrap

import logging
logging.basicConfig(format="%(module)s:%(message)s")
log = logging.getLogger(__name__)
log.setLevel(logging.WARNING)

class CodeAlong(ABC):
"""Base class for a code along"""
Expand All @@ -17,58 +24,68 @@ def __init__(self, file_name: str, sections: list[Callable[[None], None]]) -> Se
self.file_name = file_name
self._sections = sections

# TODO: Refactor
class CodeAlongWriter:
"""Class for writing the code-along files"""

def __init__(self, code_along: CodeAlong,
indent: int = 4,
indent_amount: int = 2,
# FIXME: Don't accept a code along in the `init` function, accept in a separate function
def __init__(self,
docstring_char: str = r'"""',
) -> Self:
self.code_along = code_along
self._indent_size = indent
self._indent_amount = indent_amount
self._docstring = docstring_char

def write(self, dir: Path, encoding: str = "utf-8") -> None:
def write(self, dir: Path, code_along: CodeAlong, encoding: str = "utf-8") -> None:
"""Writes the code-along content"""
for f in self.code_along._sections:
for f in code_along._sections:
file_path = dir/f"{f.__name__}.py"
content = self.func_to_filestring(f)
file_path.write_text(content, encoding=encoding)
print(f"Wrote '{file_path}'")

def _strip_indent(self, s: str) -> str:
return s.replace(r" " * self._indent_size * self._indent_amount, "")

def func_to_filestring(self, f: Callable[[None], None]) -> str:
"""Converts a Python function to the appropriately formatted string"""
"""Converts a Python function to a formatted string,
as if it were a standalone file
"""
s = StringIO()
docstring = f"\"\"\"{f.__name__}\n\n{f.__doc__}\n\"\"\"\n"
s.write(self._strip_indent(docstring))
s.write("\n")
s.write(self.parse_func_body(f))
s.write("\n")
s.write(self.func_to_module_docstring(f))
s.write(self.func_body_to_filestring(f))
log.debug(s.getvalue())
return s.getvalue()

def func_to_module_docstring(self, f: Callable[[None], None]) -> str:
"""Converts name and function docstring to module docstring"""
doc = inspect.cleandoc(f.__doc__)
return f"\"\"\"{f.__name__}\n\n{doc}\n\"\"\"\n"

def parse_func_body(self, f: Callable[[None], None]) -> str:
"""Parses the body from the function"""
# BUG: Triple indent's don't work for some reason. Need more tests
def func_body_to_filestring(self, f: Callable[[None], None]) -> str:
"""Converts the body of a function to a string, preserving identation"""
src = inspect.getsource(f)
_, src_without_doc = src.split(f.__doc__)
lines = src_without_doc.splitlines()
lines = [line.replace(r" "*self._indent_size*self._indent_amount, "") for line in lines]
i = lines.index("")
return "\n".join(lines[i+1:])
# Strip docstring
re_docstring = r'"""[\s\S]+"""\n'
signature, body = re.split(re_docstring, src)
# inspect preserves absolute indentation,
# but we want to only preserve relative identation
body = textwrap.dedent(body)
return body


class CodeAlongTester:
"""Class for testing code-alongs
Code-along files are either expected to:
For now, we assume that code blocks shouldn't error.
In the future, I might define blocks that intentionally error.
"""

1. Run without error
2. Crash and output an error
def test_for_errors(self, code_along: CodeAlong) -> None:
"""Check that the code blocks defined in a code along do not error."""
print("")

So testing code-along files is much simpler than creating a whole pytest test suite
"""
for section in code_along._sections:
# In case some example code has an intentional indefinite loop
try:
section_title = f"Testing '{section.__name__}'"
print(f"{section_title}\n{len(section_title)*"-"}")
section()
except KeyboardInterrupt as err:
print("\tKeyboard interrupt")
print()
2 changes: 1 addition & 1 deletion pftp/eng.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import logging

logging.basicConfig(level=logging.WARNING)
from combparser import ParserCombinator, ParserError
from pftp.combparser import ParserCombinator, ParserError

# A collection to hold an English expression and a roughly equivalent Python expression
EngVsPy = namedtuple("EngVsPy", ["english", "python"])
Expand Down
1 change: 0 additions & 1 deletion pftp/pymd.py

This file was deleted.

9 changes: 9 additions & 0 deletions test/codealong/hello_world_3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""hello_world_3
Module for 'Hello, World!' program - part 3
"""

greeting = "Hello, World!"
if greeting:
if greeting:
print(greeting)
65 changes: 26 additions & 39 deletions test/test_codealong.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,47 @@
import pytest
from pftp.codealong import CodeAlong, CodeAlongWriter
from pathlib import Path

@pytest.fixture()
def hello_world() -> CodeAlong:
"""An example CodeAlong writer"""
class HelloWorld(CodeAlong):

def __init__(self):
super().__init__("Hello World",
sections=[
self.hello_world_1,
self.hello_world_2,
]
)

def hello_world_1(self) -> None:
"""Module for 'Hello, World!' program - part 1"""
class TestCodeAlongWriter:

def test_func_to_str(self):
def f() -> None:
"""A function, f"""

print("Hello, World!")

def hello_world_2(self) -> None:
"""Module for 'Hello, World!' program - part 2"""

if __name__ == "__main__":
print("Hello, World!")

return HelloWorld

def test_codealong(hello_world: CodeAlong):
from pathlib import Path
writer = CodeAlongWriter()
actual = writer.func_to_filestring(f)

writer = CodeAlongWriter(hello_world(), indent_amount=3)
root_dir = Path("./test/codealong")
writer.write(root_dir)
expected = '''"""f
expected = '''"""hello_world_1
Module for 'Hello, World!' program - part 1
A function, f
"""
print("Hello, World!")
'''

assert (root_dir/"hello_world_1.py").read_text() == expected
assert actual == expected

def test_func_to_str_multiline_docstring(self):
def f() -> None:
"""
A function, f
"""

print("Hello, World!")

expected = '''"""hello_world_2
writer = CodeAlongWriter()
actual = writer.func_to_filestring(f)

Module for 'Hello, World!' program - part 2
expected = '''"""f
A function, f
"""
if __name__ == "__main__":
print("Hello, World!")
print("Hello, World!")
'''
assert actual == expected

assert (root_dir/"hello_world_2.py").read_text() == expected

def test_codealong_multiple_idents():
assert False

2 changes: 1 addition & 1 deletion test/test_parser.py → test/test_combparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def src():
Yeah, but John, if The Pirates of the Caribbean breaks down, the pirates don’t eat the tourists.
Remind me to thank John for a lovely weekend.
Do you have any idea how long it takes those cups to decompose.
Just my luck, no ice.
Just my luck, no ice.
This thing comes fully loaded.
AM/FM radio, reclining bucket seats, and... power windows."""

Expand Down

0 comments on commit 58f8157

Please sign in to comment.