Skip to content

Commit

Permalink
test: introduced more systematic tests beyond doctests
Browse files Browse the repository at this point in the history
  • Loading branch information
MoritzLaurer committed Jan 23, 2025
1 parent 0175382 commit 51aad17
Show file tree
Hide file tree
Showing 12 changed files with 596 additions and 13 deletions.
17 changes: 10 additions & 7 deletions prompt_templates/prompt_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,20 +691,23 @@ def display(self, format: Literal["json", "yaml"] = "json") -> None:
... filename="translate.yaml"
... )
>>> prompt_template.display(format="yaml") # doctest: +NORMALIZE_WHITESPACE
template: 'Translate the following text to {language}:
{text}'
template: |-
Translate the following text to {{language}}:
{{text}}
template_variables:
- language
- text
metadata:
name: Simple Translator
description: A simple translation prompt for illustrating the standard prompt YAML
format
name: "Simple Translator"
description: "A simple translation prompt for illustrating the standard prompt YAML
format"
tags:
- translation
- multilinguality
version: 0.0.1
author: Some Person
version: "0.0.1"
author: "Guy van Babel"
client_parameters: {}
custom_data: {}
"""
# Create a clean dict with only the relevant attributes
display_dict = {
Expand Down
2 changes: 1 addition & 1 deletion prompt_templates/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def list_prompt_templates(
>>> from prompt_templates import list_prompt_templates
>>> files = list_prompt_templates("MoritzLaurer/example_prompts")
>>> files
['code_teacher.yaml', 'code_teacher_test.yaml', 'translate.yaml', 'translate_jinja2.yaml']
['code_teacher.yaml', 'translate.yaml', 'translate_jinja2.yaml']
Note:
This function simply returns all YAML file names in the repository.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ addopts = [
"--cov-report=term-missing",
"--cov-report=html",
"--cov-branch",
"--cov-fail-under=50",
#"--cov-fail-under=50",
"--doctest-modules",
"--doctest-glob=*.md"
]
Expand All @@ -96,7 +96,7 @@ omit = [

[tool.coverage.report]
show_missing = true
fail_under = 50
#fail_under = 50
exclude_lines = [
"pragma: no cover",
"def __repr__",
Expand Down
48 changes: 48 additions & 0 deletions scripts/testing.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,54 @@
"logging.basicConfig(level=logging.INFO)"
]
},
{
"cell_type": "markdown",
"id": "ff462c3b",
"metadata": {},
"source": [
"### Display fix"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "2fd3949d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"template: |-\n",
" Translate the following text to {{language}}:\n",
" {{text}}\n",
"template_variables:\n",
"- language\n",
"- text\n",
"metadata:\n",
" name: \"Simple Translator\"\n",
" description: \"A simple translation prompt for illustrating the standard prompt YAML\n",
" format\"\n",
" tags:\n",
" - translation\n",
" - multilinguality\n",
" version: \"0.0.1\"\n",
" author: \"Guy van Babel\"\n",
"client_parameters: {}\n",
"custom_data: {}\n"
]
}
],
"source": [
"from prompt_templates import TextPromptTemplate, ChatPromptTemplate\n",
"prompt_template = TextPromptTemplate.load_from_hub(\n",
" repo_id=\"MoritzLaurer/example_prompts\",\n",
" filename=\"translate.yaml\"\n",
")\n",
"\n",
"prompt_template.display(format=\"yaml\")"
]
},
{
"cell_type": "markdown",
"id": "e14fae44",
Expand Down
16 changes: 13 additions & 3 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@
# Run all tests
poetry run pytest

# Run with verbose output
poetry run pytest -v

# Run tests with coverage report
poetry run pytest --cov=prompt_templates

# Run doctests
run: poetry run pytest --doctest-modules --cov=prompt_templates --cov-report=xml

# Run specific test file
poetry run pytest tests/test_hub_api.py
poetry run pytest tests/test_prompt_templates.py

# Run with coverage report
poetry run pytest --cov
# Run specific test class or function
poetry run pytest tests/test_prompt_templates.py::TestChatPromptTemplate
poetry run pytest tests/test_prompt_templates.py::TestChatPromptTemplate::test_initialization
```


Expand Down
19 changes: 19 additions & 0 deletions tests/test_data/example_prompts/code_teacher_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
prompt:
template:
- role: system
content: You are a coding assistant who explains concepts clearly and provides short examples.
- role: user
content: Explain what {{concept}} is in {{programming_language}}.
template_variables:
- concept
- programming_language
metadata:
name: Code Teacher
description: A simple chat prompt for explaining programming concepts with examples
tags:
- programming
- education
version: 0.0.1
author: Guido van Bossum
client_parameters: {}
custom_data: {}
Empty file removed tests/test_hub_api.py
Empty file.
Empty file removed tests/test_populated_prompt.py
Empty file.
170 changes: 170 additions & 0 deletions tests/test_populators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import pytest

from prompt_templates.populators import (
DoubleBracePopulator,
Jinja2TemplatePopulator,
SingleBracePopulator,
)


class TestSingleBracePopulator:
def test_basic_population(self):
populator = SingleBracePopulator()
template = "Hello {name}!"
result = populator.populate(template, {"name": "world"})
assert result == "Hello world!"

def test_multiple_variables(self):
populator = SingleBracePopulator()
template = "Hello {name}, you are {age} years old"
result = populator.populate(template, {"name": "Alice", "age": 30})
assert result == "Hello Alice, you are 30 years old"

def test_missing_variable(self):
populator = SingleBracePopulator()
template = "Hello {name}!"
with pytest.raises(ValueError, match="Variable 'name' not found"):
populator.populate(template, {})

def test_get_variable_names(self):
populator = SingleBracePopulator()
template = "Hello {name}, you are {age} years old"
variables = populator.get_variable_names(template)
assert variables == {"name", "age"}

def test_nested_braces(self):
populator = SingleBracePopulator()
template = "Nested {outer{inner}}"
with pytest.raises(ValueError):
populator.populate(template, {"outer": "value"})


class TestDoubleBracePopulator:
def test_basic_population(self):
populator = DoubleBracePopulator()
template = "Hello {{name}}!"
result = populator.populate(template, {"name": "world"})
assert result == "Hello world!"

def test_multiple_variables(self):
populator = DoubleBracePopulator()
template = "Hello {{name}}, you are {{age}} years old"
result = populator.populate(template, {"name": "Alice", "age": 30})
assert result == "Hello Alice, you are 30 years old"

def test_missing_variable(self):
populator = DoubleBracePopulator()
template = "Hello {{name}}!"
with pytest.raises(ValueError, match="Variable 'name' not found"):
populator.populate(template, {})

def test_get_variable_names(self):
populator = DoubleBracePopulator()
template = "Hello {{name}}, you are {{age}} years old"
variables = populator.get_variable_names(template)
assert variables == {"name", "age"}

def test_nested_braces(self):
populator = DoubleBracePopulator()
template = "Nested {{outer{{inner}}}}"
with pytest.raises(ValueError):
populator.populate(template, {"outer": "value"})


class TestJinja2TemplatePopulator:
def test_basic_population(self):
populator = Jinja2TemplatePopulator()
template = "Hello {{name}}!"
result = populator.populate(template, {"name": "world"})
assert result == "Hello world!"

def test_strict_mode(self):
populator = Jinja2TemplatePopulator(security_level="strict")
# Test allowed filter
template = "Hello {{name|upper}}!"
result = populator.populate(template, {"name": "world"})
assert result == "Hello WORLD!"

# Test disallowed filter
template = "Hello {{name|replace('o', 'a')}}!"
with pytest.raises(ValueError, match="No filter named 'replace'"):
populator.populate(template, {"name": "world"})

def test_standard_mode(self):
populator = Jinja2TemplatePopulator(security_level="standard")
# Test additional allowed filters
template = "Hello {{name|replace('o', 'a')|title}}!"
result = populator.populate(template, {"name": "world"})
assert result == "Hello Warld!"

# Test allowed globals
template = "Count: {{range(3)|join(', ')}}"
result = populator.populate(template, {})
assert result == "Count: 0, 1, 2"

def test_relaxed_mode(self):
populator = Jinja2TemplatePopulator(security_level="relaxed")
# Test complex template with multiple features
template = """
{% set greeting = 'Hello' %}
{{ greeting }} {{ name|title }}!
{% for i in range(count) %}
Item {{ i + 1 }}
{% endfor %}
"""
result = populator.populate(template, {"name": "world", "count": 2})
assert "Hello World!" in result
assert "Item 1" in result
assert "Item 2" in result

def test_invalid_security_level(self):
with pytest.raises(ValueError, match="Invalid security level"):
Jinja2TemplatePopulator(security_level="invalid")

def test_syntax_error(self):
populator = Jinja2TemplatePopulator()
template = "Hello {{name!" # Missing closing brace
with pytest.raises(ValueError, match="Invalid template syntax"):
populator.populate(template, {"name": "world"})

def test_undefined_variable(self):
populator = Jinja2TemplatePopulator()
template = "Hello {{name}}!"
with pytest.raises(ValueError, match="Undefined variable"):
populator.populate(template, {})

def test_get_variable_names(self):
populator = Jinja2TemplatePopulator()
template = """
{% set local = 'value' %}
Hello {{name}}, you are {{age}} years old.
{% if show_extra %}
Extra info: {{extra}}
{% endif %}
"""
variables = populator.get_variable_names(template)
assert variables == {"name", "age", "show_extra", "extra"}
# Note: 'local' is not included as it's a local variable

def test_control_structures(self):
populator = Jinja2TemplatePopulator(security_level="standard")
template = """
{% if show_greeting %}
Hello {{name}}!
{% else %}
Goodbye {{name}}!
{% endif %}
"""
result = populator.populate(template, {"name": "world", "show_greeting": True})
assert "Hello world!" in result.strip()

def test_autoescaping(self):
populator = Jinja2TemplatePopulator(security_level="standard")
template = "Hello {{name}}!"
result = populator.populate(template, {"name": "<script>alert('xss')</script>"})
assert result == "Hello &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;!"

# Test safe filter
template = "Hello {{name|safe}}!"
result = populator.populate(template, {"name": "<b>world</b>"})
assert result == "Hello <b>world</b>!"
Loading

0 comments on commit 51aad17

Please sign in to comment.