Skip to content

Commit

Permalink
start contributor experience changes
Browse files Browse the repository at this point in the history
  • Loading branch information
CalebCourier committed Feb 8, 2024
1 parent be4718e commit 33087d5
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 70 deletions.
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
dev:
pip install -e ".[dev]"

lint:
ruff check .

tests:
pytest ./test
test:
pytest ./tests

type:
pyright validator
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
# Guardrails Validator Template

## How to create a Guardrails Validator
- On the top right of the page, click "Use this template", select "create a new repository" and set a name for the package.
- On the top right of the page, click "Use this template", select "create a new repository" and set a name for the package. See [Naming Conventions](#naming-conventions) below.
- Clone down the new repository.
- Modify the class in [validator/main.py](validator/main.py) with source code for the new validator
- Make sure that the class still inherits from `Validator` and has the `register_validator` annotation.
- Set the `name` in the `register_validator` to the name of the repo and set the appropriate data type.
- Change [validator/__init__.py](validator/__init__.py) to your new Validator classname instead of RegexMatch
- Locally test the validator with the test instructions below
- Update this README to follow the Validator Card format; you can find an example [here](https://github.com/guardrails-ai/lowercase/blob/main/README.md)

* Note: This package uses a pyproject.toml file, on first run, run `pip install .` to pull down and install all dependencies
* Note: This package uses a pyproject.toml file, on first run, run `make dev` to pull down and install all dependencies

### Naming Conventions
1. Avoid using `is` and `bug`
2. Use snake_case: i.e. `_` to separate words. e.g. valid_address
3. For the description of the repo, write one sentence that says what the validator does; should be the same as the description in the pydoc string.
4. When annotating the class use the `{namespace}/{validator_name}` pattern: e.g. `@register_validator(name=“guardrails/valid_address”)`

### Testing and using your validator
- Open [test/test-validator.py](test/test-validator.py) to test your new validator
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ license = {file = "LICENSE"}
readme = "README.md"
requires-python = ">= 3.8"
dependencies = [
"rstr",
"guardrails-ai>=0.3.2"
]

Expand All @@ -24,7 +23,7 @@ dev = [
minversion = "6.0"
addopts = "-rP"
testpaths = [
"test"
"tests"
]

[tool.pyright]
Expand Down
21 changes: 0 additions & 21 deletions test/test_validator.py

This file was deleted.

Empty file added tests/__init__.py
Empty file.
27 changes: 27 additions & 0 deletions tests/test_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# to run these, run
# make tests

from guardrails import Guard
import pytest
from validator import ValidatorTemplate

# We use 'exception' as the validator's fail action,
# so we expect failures to always raise an Exception
# Learn more about corrective actions here:
# https://www.guardrailsai.com/docs/concepts/output/#%EF%B8%8F-specifying-corrective-actions
guard = Guard.from_string(validators=[ValidatorTemplate(arg_1="arg_1", arg_2="arg_2", on_fail="exception")])

def test_pass():
test_output = "pass"
result = guard.parse(test_output)

assert result.validation_passed is True
assert result.validated_output == test_output

def test_fail():
with pytest.raises(Exception) as exc_info:
test_output = "fail"
guard.parse(test_output)

# Assert the exception has your error_message
assert str(exc_info.value) == "Validation failed for field with errors: {A descriptive but concise error message about why validation failed}"
4 changes: 2 additions & 2 deletions validator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .main import RegexMatch
from .main import ValidatorTemplate

__all__ = ["RegexMatch"]
__all__ = ["ValidatorTemplate"]
58 changes: 17 additions & 41 deletions validator/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import re
import string
from typing import Any, Callable, Dict, Optional

import rstr

from guardrails.validator_base import (
FailResult,
PassResult,
Expand All @@ -13,59 +9,39 @@
)


@register_validator(name="guardrails/regex_match", data_type="string")
class RegexMatch(Validator):
"""Validates that a value matches a regular expression.
@register_validator(name="guardrails/validator_template", data_type="string")
class ValidatorTemplate(Validator):
"""Validates that {fill in how you validator interacts with the passed value}.
**Key Properties**
| Property | Description |
| ----------------------------- | --------------------------------- |
| Name for `format` attribute | `regex_match` |
| Name for `format` attribute | `guardrails/validator_template` |
| Supported data types | `string` |
| Programmatic fix | Generate a string that matches the regular expression |
| Programmatic fix | {If you support programmatic fixes, explain it here. Otherwise `None`} |
Args:
regex: Str regex pattern
match_type: Str in {"search", "fullmatch"} for a regex search or full-match option
arg_1 (string): {Description of the argument here}
arg_2 (string): {Description of the argument here}
""" # noqa

def __init__(
self,
regex: str,
match_type: Optional[str] = None,
arg_1: str,
arg_2: str,
on_fail: Optional[Callable] = None,
):
# todo -> something forces this to be passed as kwargs and therefore xml-ized.
# match_types = ["fullmatch", "search"]

if match_type is None:
match_type = "fullmatch"
assert match_type in [
"fullmatch",
"search",
], 'match_type must be in ["fullmatch", "search"]'

super().__init__(on_fail=on_fail, match_type=match_type, regex=regex)
self._regex = regex
self._match_type = match_type
super().__init__(on_fail=on_fail, arg_1=arg_1, arg_2=arg_2)
self._arg_1 = arg_1
self._arg_2 = arg_2

def validate(self, value: Any, metadata: Dict) -> ValidationResult:
p = re.compile(self._regex)
"""Validates that value matches the provided regular expression."""
# Pad matching string on either side for fix
# example if we are performing a regex search
str_padding = (
"" if self._match_type == "fullmatch" else rstr.rstr(string.ascii_lowercase)
)
self._fix_str = str_padding + rstr.xeger(self._regex) + str_padding

if not getattr(p, self._match_type)(value):
"""Validates that {fill in how you validator interacts with the passed value}."""
# Add your custom validator logic here and return a PassResult or FailResult accordingly.
if value != "pass": # FIXME
return FailResult(
error_message=f"Result must match {self._regex}",
fix_value=self._fix_str,
error_message="{A descriptive but concise error message about why validation failed}",
fix_value="{The programmtic fix if applicable, otherwise remove this kwarg.}",
)
return PassResult()

def to_prompt(self, with_keywords: bool = True) -> str:
return "results should match " + self._regex
1 change: 1 addition & 0 deletions validator/post-install.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
print("post-install starting...")
print("This is where you would do things like download nltk tokenizers or login to the HuggingFace hub...")
print("post-install complete!")
# If you don't have anything to add here you should delete this file.

0 comments on commit 33087d5

Please sign in to comment.