Commits: 11
Fix Python interpreter: leading coma when 0 args
When giving a hint to implement a step with no parameters, the generated code would contain a leading coma, like this:
@step("The following table maps words to their length")
def step_implementation_07(, **kwargs):
assert "cat" == "dog", "Obviously you need to replace this!"
Now it's smarter.
index 484bdc3..594dc1a 100644
--- a/samples/basic.py
+++ b/samples/basic.py
@@ -89,7 +89,7 @@ def ready():
= else:
= # Generate helpful hint
= indices = re.findall(r"\{(\d)\}", variant)
- params = map(lambda index: f"arg_{index}: str", indices)
+ params = list(map(lambda index: f"arg_{index}: str", indices))
= name = f"step_implementation_{len(steps_implementation):02}"
=
= hint = dedent(f"""
@@ -97,7 +97,7 @@ def ready():
=
= ``` python
= @step("{variant}")
- def {name}({str.join(", ", params)}, **kwargs):
+ def {name}({str.join(", ", params + [ "**kwargs" ])}):
= assert "cat" == "dog", "Obviously you need to replace this!"
= ```
= """)Implement support for tables
index c347baa..6536f82 100644
--- a/samples/basic.md
+++ b/samples/basic.md
@@ -36,6 +36,32 @@ Content outside of bullet points (like this) won't have any effect on the progra
= ```
=
= * There are `3` `r`s in the word `strawberry`
+ * The following table maps words to their lengths
+
+ | word | length |
+ |--------|--------|
+ | cat | 3 |
+ | stork | 5 |
+ | rabbit | 6 |
+ | snake | 5 |
+ | minx | 4 |
+
+ Any table within a step will be passed to the interpreter as a 2d array, like so:
+
+ ``` json
+ [
+ [ "word" , "length" ],
+ [ "cat" , "3" ],
+ [ "stork" , "5" ],
+ [ "rabbit" , "6" ],
+ [ "snake" , "5" ],
+ [ "minx" , "4" ]
+ ]
+ ```
+
+ Note that heading row is not treated in any special way, and all cells are strings. It's up to an interpreter to deal with it.
+
+
= * The reverse of `abc` is `cba`
= * The reverse of `CIA` is `KGB`
=index 594dc1a..4d16408 100644
--- a/samples/basic.py
+++ b/samples/basic.py
@@ -164,4 +164,13 @@ def step_implementation_06(word: str, reverse: str, **kwargs):
= actual_reverse = word[::-1]
= tester.assertEqual(reverse, actual_reverse)
=
+@step("The following table maps words to their lengths")
+def step_implementation_07(**kwargs):
+ for table in kwargs["tables"]:
+ # Skip the first row - it's a heading
+ for [word, length] in table[1:]:
+ actual_length = len(word)
+ tester.assertEqual(actual_length, int(length), f"the length of {word=}")
+
+
=ready()index 8788fab..550d8c0 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -94,6 +94,9 @@ pub struct Step {
=
= /// List of code blocks
= pub code_blocks: Vec<CodeBlock>,
+
+ /// List of tables
+ pub tables: Vec<Table>,
=}
=
=#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -113,6 +116,39 @@ impl From<&markdown::mdast::Code> for CodeBlock {
= }
=}
=
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Table(Vec<Vec<String>>);
+
+impl From<&markdown::mdast::Table> for Table {
+ fn from(table: &markdown::mdast::Table) -> Self {
+ let rows: Vec<Vec<String>> = table
+ .children
+ .iter()
+ .filter_map(|child| {
+ let markdown::mdast::Node::TableRow(_) = child else {
+ return None;
+ };
+ let rows: Vec<String> = child
+ .children()
+ .cloned()
+ .unwrap_or_default()
+ .iter()
+ .filter_map(|grandchild| {
+ let markdown::mdast::Node::TableCell(_) = grandchild else {
+ return None;
+ };
+ grandchild.to_string().into()
+ })
+ .collect();
+
+ Some(rows)
+ })
+ .collect();
+
+ Self(rows)
+ }
+}
+
=#[derive(Deserialize)]
=pub struct FrontMatter {
= pub interpreter: String,
@@ -125,6 +161,7 @@ impl Suite {
= &markdown::ParseOptions {
= constructs: markdown::Constructs {
= frontmatter: true,
+ gfm_table: true,
= ..Default::default()
= },
= ..Default::default()
@@ -263,6 +300,7 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
=
= // TODO: Extract list, tables, code blocks etc. from subsequent children of the list item
= let mut code_blocks = vec![];
+ let mut tables = vec![];
=
= for child in item.children.iter() {
= match child {
@@ -276,6 +314,11 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= code_blocks.push(CodeBlock::from(code))
= }
=
+ markdown::mdast::Node::Table(table) => {
+ log::info!("Found a table");
+ tables.push(Table::from(table));
+ }
+
= _ => continue,
= }
= }
@@ -285,6 +328,7 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= variant: variant_headline.to_string(),
= arguments,
= code_blocks,
+ tables,
= })
= }
=}Silence some noisy logging
index 550d8c0..5bf24d7 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -305,7 +305,7 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= for child in item.children.iter() {
= match child {
= markdown::mdast::Node::Code(code) => {
- log::info!(
+ log::debug!(
= "Found a code block: {language} ({meta})",
= language = code.lang.as_deref().unwrap_or("unspecified"),
= meta = code.meta.as_deref().unwrap_or_default(),
@@ -315,7 +315,7 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= }
=
= markdown::mdast::Node::Table(table) => {
- log::info!("Found a table");
+ log::debug!("Found a table");
= tables.push(Table::from(table));
= }
=Update the roadmap
index 142bd8a..bc73be3 100644
--- a/README.md
+++ b/README.md
@@ -39,13 +39,14 @@ Support for Linux, OS X and Web (WASM).
= - [ ] A summary at the bottom (esp. list of errors)
= - [x] Use colors
= - [ ] More instructive error messages
- - [ ] Indent multiline error messages
+ - [x] Indent multiline error messages
=- [ ] Pass more step data to interpreters
- - [ ] Code blocks
+ - [x] Code blocks
+ - [x] Tables
= - [ ] Lists
- - [ ] Tables
= - [ ] Definition lists
= - [ ] Original markdown fragment
+ - [ ] Block quotes
=- [x] Nix package (from Flake)
=- [ ] Use for evaluating Jewiet's Form to Mail specification
=- [ ] Helper libraries
@@ -56,6 +57,7 @@ Support for Linux, OS X and Web (WASM).
= - [x] Insightful assertion errors
= - [ ] Split library code from the basic interpreter
= - [ ] for Clojure
+ - [ ] For POSIX shells
=- [ ] Capture more data in reports
= - [ ] Attachments (screenshots, videos, datasets, etc.)
= - [ ] Performance data (interpreters' startup times, steps' durations)Bump the minor version
To celebrate tables support.
index c2e992f..a4b37ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -400,7 +400,7 @@ dependencies = [
=
=[[package]]
=name = "tad-better-behavior"
-version = "0.2.0"
+version = "0.3.0"
=dependencies = [
= "anyhow",
= "clap",index 9297cf6..32ebb5f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
=[package]
=name = "tad-better-behavior"
-version = "0.2.0"
+version = "0.3.0"
=edition = "2024"
=
=[dependencies]Promise to support BSD
For Daniel.
index bc73be3..3f727ab 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ and even the original markdown fragment itself.
=
=## Cross platform
=
-Support for Linux, OS X and Web (WASM).
+Support for Linux, BSD, OS X and Web (WASM).
=
=
=# RoadmapSeparate library code from basic.py to tbb.py
Write docstrings for public functions (register_step, step and ready).
index 4d16408..83e57d1 100644
--- a/samples/basic.py
+++ b/samples/basic.py
@@ -1,123 +1,13 @@
=#!/usr/bin/env python3
=
-import inspect
=import json
-import logging
-import os
-import re
=import unittest
-from textwrap import dedent
-from typing import Any, Callable
-
-
-# Library code
-# ============
-
-# Setup logging
-log_level = os.environ.get("tbb_interpreter_log", "warn").upper()
-logging.basicConfig(level=getattr(logging, log_level))
-log = logging.getLogger("basic_interpreter")
-
-def send(message):
- serialized = json.dumps(message)
- log.debug("Sending: %s", serialized)
- print (serialized)
-
-# This will hold all step implementations
-steps_implementation = dict()
-
-# A helper to register a step implementation
-def register_step(variant: str, implementation: Callable[..., Any]):
- log.debug("Registering step implementation for '%s'", variant)
- steps_implementation[variant] = implementation
-
-# A decorator to register a step implementation
-def step(variant: str) -> Callable[..., Any]:
- def decorator(implementation: Callable[..., Any]) -> Callable[..., Any]:
- register_step(variant, implementation)
-
- # We don't really do anything to the implementation itself
- return implementation
-
- return decorator
-
-# Call this when ready, i.e. all implementations are registered
-def ready():
- send({
- "type": "InterpreterState",
- "ready": True
- })
-
- # Loop over input
- while True:
- log.debug("Awaiting message...")
-
- try:
- received = input()
- log.debug("Received: %s", received)
- message = json.loads(received)
- if not message["type"] == "Execute":
- log.warning(f"Unexpected message received from the control program. Expected Execute type message, got {received}")
- continue
-
- step = message["step"] # The only message variant we expect
- variant = step.get("variant")
- arguments = step.get("arguments")
- log.debug(f"Looking for implementation of '{ variant }'")
- implementation = steps_implementation.get(variant)
-
- if implementation:
- log.debug(f"Found an implementation of '{ variant }'")
- try:
- signature = inspect.signature(implementation)
- log.debug(f"Signature: {signature}")
- for index, key in enumerate(signature.parameters):
- parameter = signature.parameters[key]
- log.debug(f"Processing parameter {index} ({parameter.annotation})")
- if parameter.annotation == inspect._empty:
- log.debug (f"No annotation for parameter {index} ({parameter.name}). Skipping.")
- continue
-
- log.debug (f"Converting argument {index} to {parameter.annotation.__name__}")
- arguments[index] = parameter.annotation(arguments[index])
-
- implementation(*arguments, **step)
- send({ "type": 'Success' })
- except Exception as error:
- send({ "type": "Failure", "reason": str(error) })
-
- else:
- # Generate helpful hint
- indices = re.findall(r"\{(\d)\}", variant)
- params = list(map(lambda index: f"arg_{index}: str", indices))
- name = f"step_implementation_{len(steps_implementation):02}"
-
- hint = dedent(f"""
- To get started, place the following in {__file__}:
-
- ``` python
- @step("{variant}")
- def {name}({str.join(", ", params + [ "**kwargs" ])}):
- assert "cat" == "dog", "Obviously you need to replace this!"
- ```
- """)
-
- log.warning(f"Not implemented: {variant}")
- send({
- "type": 'Failure',
- "reason": "step not implemented",
- "hint": hint
- })
-
- except EOFError:
- # The control program closed the stream.
- # Most likely it indicates the end of scenario.
- break
-
-
-# User code
-# =========
=
+import tbb
+from tbb import step, log
+
+
+# Nice assertions with helpful error messages
=tester = unittest.TestCase()
=
=@step("Add {0} and {1} to get {2}")
@@ -173,4 +63,4 @@ def step_implementation_07(**kwargs):
= tester.assertEqual(actual_length, int(length), f"the length of {word=}")
=
=
-ready()
+tbb.ready()new file mode 100644
index 0000000..0cbb852
--- /dev/null
+++ b/samples/tbb.py
@@ -0,0 +1,160 @@
+"""Reusable code useful to implement Python TBB interpreters.
+
+See `basic.py` for example.
+"""
+
+import inspect
+import json
+import logging
+import os
+import re
+from textwrap import dedent
+from typing import Any, Callable
+
+
+# Setup logging
+log_level = os.environ.get("tbb_interpreter_log", "warn").upper()
+logging.basicConfig(level=getattr(logging, log_level))
+log = logging.getLogger("basic_interpreter")
+
+def send(message):
+ serialized = json.dumps(message)
+ log.debug("Sending: %s", serialized)
+ print (serialized)
+
+# This will hold all step implementations
+steps_implementation = dict()
+
+def register_step(variant: str, implementation: Callable[..., Any]):
+ """A helper to register a step implementation
+
+ You probably want to use @step decorator instead.
+ """
+ log.debug("Registering step implementation for '%s'", variant)
+ steps_implementation[variant] = implementation
+
+def step(variant: str) -> Callable[..., Any]:
+ """ A decorator to register a step implementation
+
+ Each step has a variant, which is the text from the first line of the list
+ item, with arguments removed, e.g.
+
+ ``` markdown
+ * Make `coffee` for `2` people
+ ```
+
+ has a variant of `Make {0} for {1} people`. You can register an
+ implementation for this variant like so:
+
+ ``` python
+ @step("Make {0} for {1} people")
+ def serve(what, patron_count: int):
+ # Do what you got to do...
+ ```
+
+ We called this function serve, because another step my be:
+
+
+ ``` markdown
+ * Make `cheesecake` for `8` people
+ ```
+
+ The two steps will share the variant, and thus the same implementation.
+ Notice that our function has type annotation on the second argument
+ (patron_count), but not the first Notice that our function has type
+ annotation on the second argument (`patron_count`), but not the first.
+ That's because by default all arguments will be passed as `str`. If you
+ provide an annotation, the value will be parsed from that string.
+
+ BTW, this decorator is a thin wrapper around the `register_step` function.
+ You can use that instead, for example if you don't like decorators.
+ """
+ def decorator(implementation: Callable[..., Any]) -> Callable[..., Any]:
+ register_step(variant, implementation)
+
+ # We don't really do anything to the implementation itself
+ return implementation
+
+ return decorator
+
+def ready():
+ """Call this when your interpreter is ready.
+
+ Before calling this function make sure all step implementations are
+ registered (using the `@step` decorator) and everything is set up (e.g. you
+ might want to setup some initial state, start some processes, etc).
+
+ This function will block until the spec is evaluated. Put all the cleanup
+ code (e.g. close any process previously started) after this function call.
+ """
+
+ send({
+ "type": "InterpreterState",
+ "ready": True
+ })
+
+ # Loop over input
+ while True:
+ log.debug("Awaiting message...")
+
+ try:
+ received = input()
+ log.debug("Received: %s", received)
+ message = json.loads(received)
+ if not message["type"] == "Execute":
+ log.warning(f"Unexpected message received from the control program. Expected Execute type message, got {received}")
+ continue
+
+ step = message["step"] # The only message variant we expect
+ variant = step.get("variant")
+ arguments = step.get("arguments")
+ log.debug(f"Looking for implementation of '{ variant }'")
+ implementation = steps_implementation.get(variant)
+
+ if implementation:
+ log.debug(f"Found an implementation of '{ variant }'")
+ try:
+ signature = inspect.signature(implementation)
+ log.debug(f"Signature: {signature}")
+ for index, key in enumerate(signature.parameters):
+ parameter = signature.parameters[key]
+ log.debug(f"Processing parameter {index} ({parameter.annotation})")
+ if parameter.annotation == inspect._empty:
+ log.debug (f"No annotation for parameter {index} ({parameter.name}). Skipping.")
+ continue
+
+ log.debug (f"Converting argument {index} to {parameter.annotation.__name__}")
+ arguments[index] = parameter.annotation(arguments[index])
+
+ implementation(*arguments, **step)
+ send({ "type": 'Success' })
+ except Exception as error:
+ send({ "type": "Failure", "reason": str(error) })
+
+ else:
+ # Generate helpful hint
+ indices = re.findall(r"\{(\d)\}", variant)
+ params = list(map(lambda index: f"arg_{index}: str", indices))
+ name = f"step_implementation_{len(steps_implementation):02}"
+
+ hint = dedent(f"""
+ To get started, place the following in {__file__}:
+
+ ``` python
+ @step("{variant}")
+ def {name}({str.join(", ", params + [ "**kwargs" ])}):
+ assert "cat" == "dog", "Obviously you need to replace this!"
+ ```
+ """)
+
+ log.warning(f"Not implemented: {variant}")
+ send({
+ "type": 'Failure',
+ "reason": "step not implemented",
+ "hint": hint
+ })
+
+ except EOFError:
+ # The control program closed the stream.
+ # Most likely it indicates the end of scenario.
+ breakFix tbb.py: wrong path in the implementation hint
index 0cbb852..2a62344 100644
--- a/samples/tbb.py
+++ b/samples/tbb.py
@@ -137,8 +137,12 @@ def ready():
= params = list(map(lambda index: f"arg_{index}: str", indices))
= name = f"step_implementation_{len(steps_implementation):02}"
=
+ import importlib
+ main_module = importlib.import_module("__main__")
+ main_path = getattr(main_module, "__file__", "your interpreter script")
+
= hint = dedent(f"""
- To get started, place the following in {__file__}:
+ To get started, place the following code in {main_path}:
=
= ``` python
= @step("{variant}")Format python code, clarify a comment
index 83e57d1..11158dd 100644
--- a/samples/basic.py
+++ b/samples/basic.py
@@ -16,14 +16,12 @@ def add_and_verify(a: float, b: float, expected: float, **kwargs):
=
= tester.assertEqual(expected, a + b)
=
-
=@step("Divide {0} by {1} to get {2}")
=def divide_and_verify(a: float, b: float, expected: float, **kwargs):
= log.debug(f"{ a } / { b } = { expected }?")
=
= tester.assertAlmostEqual (expected, a / b)
=
-
=@step("The word {0} has {1} characters")
=def verify_characters_count(word, expected_length: int, **kwargs):
= log.debug(f"Is '{word}' of length {expected_length}?")
@@ -31,7 +29,6 @@ def verify_characters_count(word, expected_length: int, **kwargs):
=
= tester.assertEqual(expected_length, actual_length)
=
-
=@step("There are {0} properties in the following JSON")
=def count_json_properties(expected_count: int, **kwargs):
= code = kwargs['code_blocks'][0]['value']
@@ -43,7 +40,6 @@ def count_json_properties(expected_count: int, **kwargs):
=def step_implementation_04(a: float, b: float, expected: float, **kwargs):
= tester.assertAlmostEqual(expected, b - a)
=
-
=@step("There are {0} {1}s in the word {2}")
=def step_implementation_05(count: int, letter: str, word: str, **kwargs):
= actual_count = word.count(letter)index 2a62344..76d7073 100644
--- a/samples/tbb.py
+++ b/samples/tbb.py
@@ -11,12 +11,12 @@ import re
=from textwrap import dedent
=from typing import Any, Callable
=
-
=# Setup logging
=log_level = os.environ.get("tbb_interpreter_log", "warn").upper()
=logging.basicConfig(level=getattr(logging, log_level))
=log = logging.getLogger("basic_interpreter")
=
+
=def send(message):
= serialized = json.dumps(message)
= log.debug("Sending: %s", serialized)
@@ -161,4 +161,5 @@ def ready():
= except EOFError:
= # The control program closed the stream.
= # Most likely it indicates the end of scenario.
+ # Return control to the interpreter for any cleanup.
= breakTell git to ignore pycache
index 727883b..481d8a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
=.direnv/
=result
=
+# For python scripting
+__pycache__
+
=# Added by cargo
=
=/targetMark python library as done
index 3f727ab..d0041c3 100644
--- a/README.md
+++ b/README.md
@@ -50,12 +50,12 @@ Support for Linux, BSD, OS X and Web (WASM).
=- [x] Nix package (from Flake)
=- [ ] Use for evaluating Jewiet's Form to Mail specification
=- [ ] Helper libraries
- - [ ] for Python
+ - [x] for Python
= - [x] Proof-of-concept
= - [x] `@step` decorator
= - [x] Automatic arguments conversion (casting)
= - [x] Insightful assertion errors
- - [ ] Split library code from the basic interpreter
+ - [x] Split library code from the basic interpreter
= - [ ] for Clojure
= - [ ] For POSIX shells
=- [ ] Capture more data in reports