-
Notifications
You must be signed in to change notification settings - Fork 53
feat: add the ability to run examples with pytest #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+256
−24
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
545042c
feat: add conftest to run examples as tests
jakelorocco 51b2bae
fix: fix errors with granite guardian req generation
jakelorocco 7ccf746
fix: copy behavior with mots, add tests, add raises to genslot
jakelorocco ab80e98
fix: update codespell precommit to support ignore
jakelorocco 90ba754
Merge branch 'main' into jal/run-examples-as-tests
jakelorocco 476860c
Merge branch 'main' into jal/run-examples-as-tests
jakelorocco 48fca2d
Merge branch 'main' into jal/run-examples-as-tests
jakelorocco 75debac
Merge branch 'main' into jal/run-examples-as-tests
jakelorocco 82e7d5c
fix: add note about nbmake
jakelorocco cbfa395
Merge branch 'main' into jal/run-examples-as-tests
jakelorocco 5c3c304
Merge branch 'main' into jal/run-examples-as-tests
jakelorocco File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| """Allows you to use `pytest docs` to run the examples.""" | ||
|
|
||
| import pathlib | ||
| import subprocess | ||
| import sys | ||
|
|
||
| import pytest | ||
|
|
||
| examples_to_skip = { | ||
| "101_example.py", | ||
| "__init__.py", | ||
| "simple_rag_with_filter.py", | ||
| "mcp_example.py", | ||
| "client.py", | ||
| } | ||
|
|
||
|
|
||
| def pytest_terminal_summary(terminalreporter, exitstatus, config): | ||
| # Append the skipped examples if needed. | ||
| if len(examples_to_skip) == 0: | ||
| return | ||
|
|
||
| terminalreporter.ensure_newline() | ||
| terminalreporter.section("Skipped Examples", sep="=", blue=True, bold=True) | ||
| terminalreporter.line( | ||
| f"Examples with the following names were skipped because they cannot be easily run in the pytest framework; please run them manually:\n{'\n'.join(examples_to_skip)}" | ||
| ) | ||
|
|
||
|
|
||
| # This doesn't replace the existing pytest file collection behavior. | ||
| def pytest_collect_file(parent: pytest.Dir, file_path: pathlib.PosixPath): | ||
| # Do a quick check that it's a .py file in the expected `docs/examples` folder. We can make | ||
| # this more exact if needed. | ||
| if ( | ||
| file_path.suffix == ".py" | ||
| and "docs" in file_path.parts | ||
| and "examples" in file_path.parts | ||
| ): | ||
| # Skip this test. It requires additional setup. | ||
| if file_path.name in examples_to_skip: | ||
| return | ||
|
|
||
| return ExampleFile.from_parent(parent, path=file_path) | ||
|
|
||
| # TODO: Support running jupyter notebooks: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: you could do this with |
||
| # - use nbmake or directly use nbclient as documented below | ||
| # - install the nbclient package | ||
| # - run either using python api or jupyter execute | ||
| # - must replace background processes | ||
| # if file_path.suffix == ".ipynb": | ||
| # return ExampleFile.from_parent(parent, path=file_path) | ||
|
|
||
|
|
||
| class ExampleFile(pytest.File): | ||
| def collect(self): | ||
| return [ExampleItem.from_parent(self, name=self.name)] | ||
|
|
||
|
|
||
| class ExampleItem(pytest.Item): | ||
| def __init__(self, **kwargs): | ||
| super().__init__(**kwargs) | ||
|
|
||
| def runtest(self): | ||
| process = subprocess.Popen( | ||
| [sys.executable, self.path], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| text=True, | ||
| bufsize=1, # Enable line-buffering | ||
| ) | ||
|
|
||
| # Capture stdout output and output it so it behaves like a regular test with -s. | ||
| stdout_lines = [] | ||
| if process.stdout is not None: | ||
| for line in process.stdout: | ||
| sys.stdout.write(line) | ||
| sys.stdout.flush() # Ensure the output is printed immediately | ||
| stdout_lines.append(line) | ||
| process.stdout.close() | ||
|
|
||
| retcode = process.wait() | ||
|
|
||
| # Capture stderr output. | ||
| stderr = "" | ||
| if process.stderr is not None: | ||
| stderr = process.stderr.read() | ||
|
|
||
| if retcode != 0: | ||
| raise ExampleTestException( | ||
| (f"Example failed with exit code {retcode}.\nStderr: {stderr}\n") | ||
| ) | ||
|
|
||
| def repr_failure(self, excinfo, style=None): | ||
| """Called when self.runtest() raises an exception.""" | ||
| if isinstance(excinfo.value, ExampleTestException): | ||
| return str(excinfo.value) | ||
|
|
||
| return super().repr_failure(excinfo) | ||
|
|
||
| def reportinfo(self): | ||
| return self.path, 0, f"usecase: {self.name}" | ||
|
|
||
|
|
||
| class ExampleTestException(Exception): | ||
| """Custom exception for error reporting.""" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import copy | ||
| import pytest | ||
|
|
||
| from mellea.backends.types import ModelOption | ||
| from mellea.stdlib.base import ModelOutputThunk | ||
| from mellea.stdlib.session import MelleaSession, start_session | ||
|
|
||
| # Use generated ModelOutputThunks to fully test copying. This can technically be done without a | ||
| # backend, but it simplifies test setup. | ||
| @pytest.fixture(scope="module") | ||
| def m_session(gh_run): | ||
| if gh_run == 1: | ||
| m = start_session( | ||
| "ollama", | ||
| model_id="llama3.2:1b", | ||
| model_options={ModelOption.MAX_NEW_TOKENS: 5}, | ||
| ) | ||
| else: | ||
| m = start_session( | ||
| "ollama", | ||
| model_id="granite3.3:8b", | ||
| model_options={ModelOption.MAX_NEW_TOKENS: 5}, | ||
| ) | ||
| yield m | ||
| del m | ||
|
|
||
| def test_model_output_thunk_copy(m_session: MelleaSession): | ||
| """Basic tests for copying ModelOutputThunk. Add checks if needed.""" | ||
| out = m_session.instruct("Hello!") | ||
| copied = copy.copy(out) | ||
|
|
||
| assert out is not copied | ||
| assert copied._generate is None | ||
| assert copied._meta is out._meta | ||
|
|
||
| empty = ModelOutputThunk("") | ||
| copy.copy(empty) # Make sure no errors happen. | ||
|
|
||
|
|
||
|
|
||
| def test_model_output_thunk_deepcopy(m_session: MelleaSession): | ||
| """Basic tests for deepcopying ModelOutputThunk. Add checks if needed.""" | ||
| out = m_session.instruct("Goodbye!") | ||
| deepcopied = copy.deepcopy(out) | ||
|
|
||
| assert out is not deepcopied | ||
| assert deepcopied._generate is None | ||
| assert deepcopied._meta is not out._meta | ||
|
|
||
| empty = ModelOutputThunk("") | ||
| copy.deepcopy(empty) # Make sure no errors happen. | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| pytest.main([__file__]) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want all
conftest.pyto be in the same place. Not sure how the hierarchy works with pytestThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe eventually, but I don't think so right now:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it requires changing how the pytest command is invoked. I think that change is fine, but I we should think about when we want to run the examples, etc...