├── stoplight ├── tests │ ├── __init__.py │ └── test_validation.py ├── exceptions.py ├── rule.py ├── __init__.py └── decorators.py ├── tools └── test-requires ├── AUTHORS ├── setup.cfg ├── .coveragerc ├── .travis.yml ├── tox.ini ├── doc ├── stoplight.tests.rst ├── index.rst ├── stoplight.rst ├── Makefile ├── make.bat └── conf.py ├── .gitignore ├── .github └── workflows │ └── python-package.yml ├── setup.py ├── README.md └── LICENSE /stoplight/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/test-requires: -------------------------------------------------------------------------------- 1 | coverage==4.4.1 2 | mock==2.0 3 | six==1.11.0 4 | pytest-cov==2.5.1 5 | pytest==3.3.2 6 | testtools==2.3.0 7 | pycodestyle==2.3.1 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | 2 | * Jamie Painter (painterjd) 3 | * Benjamen Meyer (BenjamenMeyer) 4 | * Tony Tan (tonytan4ever) 5 | * Sriram Madapusi Vasudevan (TheSriram) 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | match=^test 3 | where=stoplight 4 | with-coverage=true 5 | cover-package=stoplight 6 | cover-erase=1 7 | cover-inclusive=true 8 | cover-branches=true 9 | cover-min-percentage=100 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | # Have to re-enable the standard pragma 4 | pragma: no cover 5 | 6 | # Don't complain if tests don't hit defensive assertion code: 7 | raise NotImplementedError 8 | 9 | [run] 10 | omit = *tests* 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: pip install tox 3 | env: 4 | - TOX_ENV=py27 5 | - TOX_ENV=py33 6 | - TOX_ENV=py34 7 | - TOX_ENV=py35 8 | - TOX_ENV=py37 9 | - TOX_ENV=pypy 10 | - TOX_ENV=pep8 11 | 12 | script: tox -e $TOX_ENV 13 | notifications: 14 | email: 15 | - accts@jpainter.org 16 | - bm_witness@yahoo.com 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.0 3 | envlist = py27,{py33,py34,py35,py36,py37},pypy,pep8 4 | skip_missing_interpreters=True 5 | 6 | [testenv] 7 | deps = -r{toxinidir}/tools/test-requires 8 | commands = 9 | pytest {toxinidir}/stoplight/tests --cov-config=.coveragerc --cov=stoplight {posargs} 10 | 11 | [testenv:pep8] 12 | deps = pycodestyle 13 | 14 | #NOTE: E128 = Visual indent 15 | commands = pycodestyle --exclude=.tox,dist,doc,.env --ignore=E128 16 | -------------------------------------------------------------------------------- /doc/stoplight.tests.rst: -------------------------------------------------------------------------------- 1 | stoplight.tests package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | stoplight.tests.test_validation module 8 | -------------------------------------- 9 | 10 | .. automodule:: stoplight.tests.test_validation 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: stoplight.tests 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. stoplight documentation master file, created by 2 | sphinx-quickstart on Thu Mar 12 16:44:14 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to stoplight's documentation! 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 4 13 | 14 | stoplight 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.c 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .cache 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | *.dat 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Idea 36 | .idea 37 | 38 | # System 39 | .DS_Store 40 | 41 | # VIM swap files 42 | .*.swp 43 | 44 | # VIM temp files 45 | *~ 46 | -------------------------------------------------------------------------------- /doc/stoplight.rst: -------------------------------------------------------------------------------- 1 | stoplight package 2 | ================= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | stoplight.tests 10 | 11 | Submodules 12 | ---------- 13 | 14 | stoplight.decorators module 15 | --------------------------- 16 | 17 | .. automodule:: stoplight.decorators 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | stoplight.exceptions module 23 | --------------------------- 24 | 25 | .. automodule:: stoplight.exceptions 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | stoplight.rule module 31 | --------------------- 32 | 33 | .. automodule:: stoplight.rule 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: stoplight 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /stoplight/exceptions.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | class ValidationFailed(ValueError): 5 | """User input was inconsistent with API restrictions""" 6 | 7 | def __init__(self, msg, *args, **kwargs): 8 | if len(args) or len(kwargs): 9 | warnings.warn( 10 | 'It is recommended to format the Exception message ' 11 | 'outside the parameters to the Exception.', 12 | DeprecationWarning 13 | ) 14 | msg = msg.format(*args, **kwargs) 15 | super(ValidationFailed, self).__init__(msg) 16 | 17 | 18 | class ValidationProgrammingError(ValueError): 19 | """Caller did not map validations correctly""" 20 | 21 | def __init__(self, msg, *args, **kwargs): 22 | if len(args) or len(kwargs): 23 | warnings.warn( 24 | 'It is recommended to format the Exception message ' 25 | 'outside the parameters to the Exception.', 26 | DeprecationWarning 27 | ) 28 | msg = msg.format(*args, **kwargs) 29 | super(ValidationProgrammingError, self).__init__(msg) 30 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10","3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install coverage mock six pytest-cov pytest testtools pycodestyle 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with pycodestyle 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | pycodestyle --exclude=.tox,dist,doc,.env --ignore=E128 36 | - name: Test with pytest 37 | run: | 38 | pytest stoplight/tests --cov-config=.coveragerc --cov=stoplight 39 | -------------------------------------------------------------------------------- /stoplight/rule.py: -------------------------------------------------------------------------------- 1 | 2 | from collections import namedtuple 3 | import inspect 4 | import stoplight 5 | 6 | 7 | class Rule(object): 8 | 9 | def __init__(self, vfunc, errfunc, getter=None, nested_rules=None): 10 | """Constructs a single validation rule. A rule effectively 11 | is saying "I want to validation this input using 12 | this function and if validation fails I want this (on_error) 13 | to happen. 14 | 15 | :param vfunc: The function used to validate this param 16 | :param on_error: The function to call when an error is detected 17 | :param value_src: The source from which the value can be 18 | This function should take a value as a field name 19 | as a single param. 20 | """ 21 | self._vfunc = vfunc 22 | self._errfunc = errfunc 23 | self._getter = getter 24 | self._nested = nested_rules or [] 25 | 26 | @property 27 | def vfunc(self): 28 | return self._vfunc 29 | 30 | @property 31 | def errfunc(self): 32 | return self._errfunc 33 | 34 | @property 35 | def getter(self): 36 | return self._getter 37 | 38 | @property 39 | def nested_rules(self): 40 | return self._nested 41 | 42 | def call_error(self, failure_info): 43 | """Helper function that calls the error function, optionally 44 | passing the failure_info parameter to the error handler 45 | if it expects a parameter""" 46 | 47 | try: 48 | spec = inspect.getfullargspec(self.errfunc) 49 | except AttributeError: 50 | # running on Python 2 51 | spec = inspect.getargspec(self.errfunc) 52 | if spec.args == []: 53 | self.errfunc() 54 | else: 55 | self.errfunc(failure_info) 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | from setuptools import setup, find_packages 4 | except ImportError: 5 | from ez_setup import use_setuptools 6 | use_setuptools() 7 | from setuptools import setup, find_packages 8 | 9 | REQUIRES = ['six'] 10 | 11 | with open('README.md') as description_input: 12 | LONG_DESCRIPTION = description_input.read() 13 | 14 | setup( 15 | name='stoplight', 16 | version='1.4.1', 17 | description='Input validation framework for Python', 18 | long_description=LONG_DESCRIPTION, 19 | long_description_content_type='text/x-md', 20 | license='Apache License 2.0', 21 | url='https://pypi.org/project/stoplight/', 22 | platforms=['OS Independent'], 23 | project_urls={ 24 | 'Documentation': 'http://stoplight.readthedocs.io/en/latest/', 25 | 'Source': 'https://github.com/painterjd/stoplight', 26 | 'Tracker': 'https://github.com/painterjd/stoplight/issues', 27 | }, 28 | author='Jamie Painter', 29 | author_email='jamie.painter@rackspace.com', 30 | maintainer='Benjamen Meyer', 31 | maintainer_email='bm_witness@yahoo.com', 32 | install_requires=REQUIRES, 33 | test_suite='stoplight', 34 | zip_safe=False, 35 | include_package_data=True, 36 | packages=find_packages(exclude=['ez_setup', 'stoplight/tests']), 37 | classifiers=[ 38 | 'Development Status :: 5 - Production/Stable', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: Apache Software License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3.3', 45 | 'Programming Language :: Python :: 3.4', 46 | 'Programming Language :: Python :: 3.5', 47 | 'Programming Language :: Python :: 3.6', 48 | 'Programming Language :: Python :: 3.7', 49 | 'Topic :: Software Development :: Libraries :: Python Modules', 50 | 'Topic :: Security' 51 | ] 52 | ) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stoplight [![Build](https://github.com/painterjd/stoplight/actions/workflows/python-package.yml/badge.svg)](https://github.com/painterjd/stoplight/actions/workflows/python-package.yml) 2 | ========= 3 | 4 | Stoplight -- An Input Validation Framework for Python 5 | 6 | Why Validate User Input? 7 | ------------------------ 8 | Input validation is the most basic, first step to adding security to any application that accepts untrusted user input. Volumes have been written on the subject, but the gist is to reduce the attack surface of your application by sanitizing all user input so that it meets a very tight set of criteria needed just for the application, and nothing more. The most common type of attack prevented by input validation is [Code Injection](http://en.wikipedia.org/wiki/Code_injection). 9 | 10 | A great number of user input vulnerabilities (i.e. [Shellshock](http://en.wikipedia.org/wiki/Shellshock_%28software_bug%29)) could be avoided almost entirely if user input were sanitized. 11 | 12 | Example 13 | ------- 14 | Let's say that our application is accepting a US Phone Number only. In that case, our application should only need to accept NNN-NNN-NNNN where N is a digit from 0-9. If the user passes anything else, we can throw it away. 15 | 16 | The problem that stoplight aims to address is the intermixing of input validation logic with application logic (in particular with RESTful/REST-like API frameworks). Sometimes they are inseparable, but in almost all cases, they are not. So let's look at the above-mentioned phone number example. 17 | 18 | Almost all of today's API frameworks work in a similar manner -- you declare a function that defines an endpoint and the framework calls the function when an HTTP request comes in from a client. 19 | 20 | ```python 21 | def post(self, account_id, phone_number): 22 | if not is_valid_account_id(account_id): 23 | handle_bad_account_id() 24 | 25 | if not is_valid_phone_number(phone_number): 26 | handle_bad_phone_number() 27 | 28 | model.set_phone_number(account_id, phone_number) 29 | ``` 30 | 31 | This is a simple, contrived example. Typically things start getting much more complex. For certain HTTP verbs, a user will want different responses returned. There may be other things to accomplish as well. 32 | 33 | In Stoplight, we would validate the input like so: 34 | 35 | ```python 36 | @validate(account_id=AccountIdRule, phone_number=PhoneNumberRule) 37 | def post(self, account_id, phone_number): 38 | model.set_phone_number(account_id, phone_number) 39 | ``` 40 | 41 | This allows us to effectively separate our "input validation" logic from "business logic". 42 | 43 | Rules are fairly simple to create. For example, here is how one might declare the PhoneNumberRule 44 | 45 | ```python 46 | PhoneNumberRule = Rule(is_validate_phone_number(), lambda: abort(404)) 47 | ``` 48 | 49 | And of course, that leads us to is_valid_phone_number() declaration. 50 | 51 | ```python 52 | @validation_function 53 | def is_valid_phone_number(candidate): 54 | if (phone_regex.match(candidate) is None): 55 | msg = 'Not a valid phone number: {0}' 56 | msg = msg.format(candidate) 57 | raise ValidationFailed(msg) 58 | ``` 59 | 60 | This allows us to separate validation from transports (imagine an API where you must support HTTP and ZMQ, for example). It also allows us to centralize validation logic and write separate tests for the validation rules. 61 | 62 | Other Features: 63 | --------------- 64 | * Ensures that all parameters (positional and keyword) are all validated. If they are not validated, a ValidationProgrammingError is raised. 65 | * Allows validation of globally-scoped values (think items in thread local storage, as is done in the Pecan framework) 66 | 67 | Caveats (TODO): 68 | --------------- 69 | * Overhead. Such is the nature of Python with decorators. 70 | 71 | Documentation: 72 | -------------- 73 | The project is being documented at readthedocs [here](http://stoplight.readthedocs.org/en/latest/). For other examples, please see the unit tests. 74 | -------------------------------------------------------------------------------- /stoplight/__init__.py: -------------------------------------------------------------------------------- 1 | """Stoplight -- an input validation framework for Python 2 | 3 | Every good programmer should know that input validation is the first and 4 | best step at implementing application-level security for your product. 5 | Unvalidated user input leads to issues such as SQL injection, javascript 6 | injection and cross-site scripting attacks, etc. 7 | 8 | More and more applications are being written for python. Unfortunately, 9 | not many frameworks provide for reasonable input validation techniques 10 | and when the do, the frameworks tend further tie your application 11 | into that framework. 12 | 13 | For more complex projects that must supply more input validations, a 14 | frame-work based validation framework becomes even more useless because 15 | the validations must be done in different ways for each transport, 16 | meaning that the chance of a programmer missing a crucial validation 17 | is greatly increased. 18 | 19 | A very common programming paradigm for wsgi-based applications is for 20 | applications to expose RESTful endpoints as method members of a 21 | controller class. Typical input validation results in logic built-in 22 | to each function. This makes validating the input very tedious. 23 | 24 | Stoplight aims to provide a nice, convenient and flexible way to 25 | validate user input using a simple decorator. 26 | """ 27 | 28 | import inspect 29 | 30 | from stoplight.rule import * 31 | from stoplight.exceptions import * 32 | from stoplight.decorators import * 33 | 34 | _callbacks = set() 35 | 36 | 37 | class ValidationFailureInfo(object): 38 | """Describes information related to a particular validation 39 | failure.""" 40 | 41 | def __init__(self, **kwargs): 42 | self._function = kwargs.get("function") 43 | self._rule = kwargs.get("rule") 44 | self._nested_failure = kwargs.get("nested_failure") 45 | self._param = kwargs.get("parameter") 46 | self._param_value = kwargs.get("parameter_value") 47 | self._param_value = kwargs.get("ex") 48 | 49 | @property 50 | def function(self): 51 | """The function whose input was being validated.""" 52 | return self._function 53 | 54 | @function.setter 55 | def function(self, value): 56 | self._function = value 57 | 58 | @property 59 | def rule(self): 60 | """The rule that generated the error.""" 61 | return self._rule 62 | 63 | @rule.setter 64 | def rule(self, value): 65 | self._rule = value 66 | 67 | @property 68 | def nested_failure(self): 69 | return self._nested_failure 70 | 71 | @nested_failure.setter 72 | def nested_failure(self, value): 73 | self._nested_failure = value 74 | 75 | @property 76 | def parameter(self): 77 | """The name of the parameter that failed validation.""" 78 | return self._param 79 | 80 | @parameter.setter 81 | def parameter(self, value): 82 | self._param = value 83 | 84 | @property 85 | def parameter_value(self): 86 | """The value that was passed to the parameter""" 87 | return self._param_value 88 | 89 | @parameter_value.setter 90 | def parameter_value(self, value): 91 | self._param_value = value 92 | 93 | @property 94 | def ex(self): 95 | """The exception that was thrown by the validation function""" 96 | return self._ex 97 | 98 | @ex.setter 99 | def ex(self, value): 100 | self._ex = value 101 | 102 | def __str__(self): 103 | msg = "Validation Failed [" 104 | msg += "filename={0}, ".format( 105 | inspect.getsourcefile(self.function)) 106 | msg += "function={0}, ".format( 107 | self.function.__name__) 108 | msg += "rule={0}, ".format(self.rule.__class__.__name__) 109 | msg += "param={0}, ".format(self.parameter) 110 | msg += "param_value={0}, ".format(self.parameter_value) 111 | 112 | if self.nested_failure is not None: # pragma: no cover 113 | msg += "nested_failure={0}".format(self.nested_failure) 114 | 115 | msg += "ex={0}".format(self.ex) 116 | msg += "]" 117 | 118 | return msg 119 | 120 | 121 | def register_callback(callback_func): 122 | """This function will register a callback to be called in case 123 | a rule fails to validate input. 124 | 125 | This function will be called with information about each failure. The 126 | validation callback function should expect a single variable which 127 | will be a ValidationFailureInformation object. 128 | 129 | This functionality is intended for and probably most useful for logging. 130 | """ 131 | global _callbacks 132 | _callbacks.add(callback_func) 133 | 134 | 135 | def unregister_callback(callback_func): 136 | """Unregisters the specified callback function""" 137 | if callback_func in _callbacks: 138 | _callbacks.remove(callback_func) 139 | 140 | 141 | def failure_dispatch(failureinfo): 142 | """Sends the specified failure information to all registered callback 143 | handlers. 144 | 145 | :param failureinfo: An instance of ValidationFailureInfo describing the 146 | failure 147 | """ 148 | global _callbacks 149 | 150 | for cb in _callbacks: 151 | assert isinstance(failureinfo, ValidationFailureInfo) 152 | 153 | try: 154 | cb(failureinfo) 155 | except Exception as ex: 156 | # If a particular callback throws an exception, we do not want 157 | # that to prevent subsequent callbacks from happening, so we 158 | # catch and squash this error and write it to stderr 159 | import sys 160 | sys.stderr.write("ERROR: Dispatch function threw an exception.") 161 | sys.stderr.write(str(ex)) 162 | sys.stderr.flush() 163 | -------------------------------------------------------------------------------- /stoplight/decorators.py: -------------------------------------------------------------------------------- 1 | 2 | import inspect 3 | from functools import wraps 4 | import stoplight 5 | from stoplight.exceptions import * 6 | from stoplight.rule import * 7 | 8 | 9 | def _apply_rule(func, rule, param, getter=None, dispatch=True): 10 | """Helper function that takes a given rule and value 11 | and attempts to perform validation on this rule 12 | 13 | :param func: The decorated function for which we are validating input 14 | :param rule: The rule to validate 15 | :param param: The parameter that we're validating, if applicable. 16 | Otherwise, None 17 | :param getter: A function for retrieving the value to validate 18 | :param outerrule: If specified, this is an outerrule 19 | 20 | Returns True if the rule was successfully applied and the 21 | validate succeeded. Returns False if the validation was 22 | performed and failed. Raises a ValidationProgrammingException 23 | otherwise 24 | """ 25 | def _create_failure_info(): 26 | val_failure = stoplight.ValidationFailureInfo() 27 | val_failure.function = func 28 | val_failure.parameter = param 29 | val_failure.parameter_value = value 30 | val_failure.rule = rule 31 | 32 | return val_failure 33 | 34 | # Are we using a getter other than the one specified in the rule? 35 | g = getter or rule.getter 36 | 37 | value = g(param) 38 | 39 | try: 40 | resp = rule.vfunc(value) 41 | 42 | # Ensure that the validation function did not return 43 | # anything. This is to ensure that it is not a function 44 | if resp is not None and inspect.isfunction(resp): 45 | msg = 'Val func returned a function. Rule={0}' 46 | msg = msg.format(rule.__class__.__name__) 47 | 48 | raise ValidationProgrammingError(msg) 49 | 50 | except ValidationFailed as ex: 51 | val_failure = _create_failure_info() 52 | val_failure.ex = ex 53 | 54 | # We always call the error handler at the point point of 55 | # failure (even if it's a nested rule), then dispatch at 56 | # the outer-most level 57 | rule.call_error(val_failure) 58 | 59 | if dispatch is True: 60 | stoplight.failure_dispatch(val_failure) 61 | 62 | return val_failure 63 | 64 | # Validation on the outer rule was successful, 65 | for nested_rule in rule.nested_rules: 66 | 67 | # We step through each nested rule, performing the same type of 68 | out = _apply_rule(func, nested_rule, value, 69 | nested_rule.getter, dispatch=False) 70 | 71 | if out is not None: 72 | val_failure = _create_failure_info() 73 | val_failure.nested_failure = out 74 | val_failure.ex = out.ex 75 | 76 | stoplight.failure_dispatch(val_failure) 77 | 78 | return val_failure 79 | 80 | # NOTE: If we reach this point, validation was successful 81 | 82 | 83 | def validate(*freerules, **paramrules): 84 | """Validates a function's input using the specified set of paramrules.""" 85 | def _validate(f): 86 | @wraps(f) 87 | def wrapper(*args, **kwargs): 88 | # Validate the free rules 89 | for rule in freerules: 90 | # Free rules *must* have a getter function to return 91 | # a value that can be passed into the validation 92 | # function 93 | if rule.getter is None: 94 | msg = "Free rules must specify a getter. Rule={0}" 95 | msg = msg.format(rule.__class__.__name__) 96 | raise ValidationProgrammingError(msg) 97 | 98 | if _apply_rule(f, rule, None, rule.getter) is not None: 99 | return 100 | 101 | try: 102 | funcparams = inspect.getfullargspec(f) 103 | except AttributeError: 104 | # running on Python 2 105 | funcparams = inspect.getargspec(f) 106 | 107 | # Holds the list of validated values. Only 108 | # these values are passed to the decorated function 109 | outargs = dict() 110 | 111 | param_map = list(zip(funcparams.args, args)) 112 | 113 | # Create dictionary that maps parameters passed 114 | # to their values passed 115 | param_values = dict(param_map) 116 | 117 | # Bring in kwargs so that we can validate those as well. 118 | param_values.update(kwargs) 119 | 120 | # Now check for rules and parameters. We should have one 121 | # rule for every parameter. 122 | param_names = set(param_values.keys()) 123 | rule_names = set(paramrules.keys()) 124 | 125 | missing_rules = list(param_names - rule_names) 126 | 127 | # TODO: for optimization, move this out to a 128 | # variable since it's immutable for our purposes 129 | if missing_rules not in [[], ['self']]: 130 | msg = "Parameter(s) not validated {0}" 131 | msg = msg.format(missing_rules) 132 | raise ValidationProgrammingError(msg) 133 | 134 | unassigned_rules = list(rule_names - param_names) 135 | 136 | if unassigned_rules != []: 137 | msg = "No such parameter for rule(s) {0}" 138 | msg = msg.format(unassigned_rules) 139 | raise ValidationProgrammingError(msg) 140 | 141 | for param, rule in paramrules.items(): 142 | 143 | # Where can we get the value? It's either 144 | # the getter on the rule or we default 145 | # to verifying parameters. 146 | getval = rule.getter or param_values.get 147 | 148 | if _apply_rule(f, rule, param, getval) is not None: 149 | # Validation was not successful 150 | return 151 | 152 | # Validation was successful, call the wrapped function 153 | return f(*args, **kwargs) 154 | return wrapper 155 | return _validate 156 | 157 | 158 | def validation_function(func): 159 | """Decorator for creating a validation function""" 160 | @wraps(func) 161 | def inner(none_ok=False, empty_ok=False): 162 | def wrapper(value, **kwargs): 163 | if none_ok is True and value is None: 164 | return 165 | 166 | if none_ok is not True and value is None: 167 | msg = 'None value not permitted' 168 | raise ValidationFailed(msg) 169 | 170 | if empty_ok is True and value == '': 171 | return 172 | 173 | if empty_ok is not True and value == '': 174 | msg = 'Empty value not permitted' 175 | raise ValidationFailed(msg) 176 | 177 | func(value) 178 | return wrapper 179 | return inner 180 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/stoplight.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/stoplight.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/stoplight" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/stoplight" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\stoplight.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\stoplight.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # stoplight documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Mar 12 16:44:14 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.todo', 35 | 'sphinx.ext.viewcode', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix(es) of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'stoplight' 52 | copyright = u'2015, Rackspace' 53 | author = u'Rackspace' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '1.2.0' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '1.2.0' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = 'en' 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = True 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'default' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | #html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | #html_sidebars = {} 159 | 160 | # Additional templates that should be rendered to pages, maps page names to 161 | # template names. 162 | #html_additional_pages = {} 163 | 164 | # If false, no module index is generated. 165 | #html_domain_indices = True 166 | 167 | # If false, no index is generated. 168 | #html_use_index = True 169 | 170 | # If true, the index is split into individual pages for each letter. 171 | #html_split_index = False 172 | 173 | # If true, links to the reST sources are added to the pages. 174 | #html_show_sourcelink = True 175 | 176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 177 | #html_show_sphinx = True 178 | 179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages will 183 | # contain a tag referring to it. The value of this option must be the 184 | # base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Language to be used for generating the HTML full-text search index. 191 | # Sphinx supports the following languages: 192 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 193 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 194 | #html_search_language = 'en' 195 | 196 | # A dictionary with options for the search language support, empty by default. 197 | # Now only 'ja' uses this config value 198 | #html_search_options = {'type': 'default'} 199 | 200 | # The name of a javascript file (relative to the configuration directory) that 201 | # implements a search results scorer. If empty, the default will be used. 202 | #html_search_scorer = 'scorer.js' 203 | 204 | # Output file base name for HTML help builder. 205 | htmlhelp_basename = 'stoplightdoc' 206 | 207 | # -- Options for LaTeX output --------------------------------------------- 208 | 209 | latex_elements = { 210 | # The paper size ('letterpaper' or 'a4paper'). 211 | #'papersize': 'letterpaper', 212 | 213 | # The font size ('10pt', '11pt' or '12pt'). 214 | #'pointsize': '10pt', 215 | 216 | # Additional stuff for the LaTeX preamble. 217 | #'preamble': '', 218 | 219 | # Latex figure (float) alignment 220 | #'figure_align': 'htbp', 221 | } 222 | 223 | # Grouping the document tree into LaTeX files. List of tuples 224 | # (source start file, target name, title, 225 | # author, documentclass [howto, manual, or own class]). 226 | latex_documents = [ 227 | (master_doc, 'stoplight.tex', u'stoplight Documentation', 228 | u'Rackspace', 'manual'), 229 | ] 230 | 231 | # The name of an image file (relative to this directory) to place at the top of 232 | # the title page. 233 | #latex_logo = None 234 | 235 | # For "manual" documents, if this is true, then toplevel headings are parts, 236 | # not chapters. 237 | #latex_use_parts = False 238 | 239 | # If true, show page references after internal links. 240 | #latex_show_pagerefs = False 241 | 242 | # If true, show URL addresses after external links. 243 | #latex_show_urls = False 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #latex_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #latex_domain_indices = True 250 | 251 | 252 | # -- Options for manual page output --------------------------------------- 253 | 254 | # One entry per manual page. List of tuples 255 | # (source start file, name, description, authors, manual section). 256 | man_pages = [ 257 | (master_doc, 'stoplight', u'stoplight Documentation', 258 | [author], 1) 259 | ] 260 | 261 | # If true, show URL addresses after external links. 262 | #man_show_urls = False 263 | 264 | 265 | # -- Options for Texinfo output ------------------------------------------- 266 | 267 | # Grouping the document tree into Texinfo files. List of tuples 268 | # (source start file, target name, title, author, 269 | # dir menu entry, description, category) 270 | texinfo_documents = [ 271 | (master_doc, 'stoplight', u'stoplight Documentation', 272 | author, 'stoplight', 'One line description of project.', 273 | 'Miscellaneous'), 274 | ] 275 | 276 | # Documents to append as an appendix to all manuals. 277 | #texinfo_appendices = [] 278 | 279 | # If false, no module index is generated. 280 | #texinfo_domain_indices = True 281 | 282 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 283 | #texinfo_show_urls = 'footnote' 284 | 285 | # If true, do not generate a @detailmenu in the "Top" node's menu. 286 | #texinfo_no_detailmenu = False 287 | 288 | 289 | # -- Options for Epub output ---------------------------------------------- 290 | 291 | # Bibliographic Dublin Core info. 292 | epub_title = project 293 | epub_author = author 294 | epub_publisher = author 295 | epub_copyright = copyright 296 | 297 | # The basename for the epub file. It defaults to the project name. 298 | #epub_basename = project 299 | 300 | # The HTML theme for the epub output. Since the default themes are not optimized 301 | # for small screen space, using the same theme for HTML and epub output is 302 | # usually not wise. This defaults to 'epub', a theme designed to save visual 303 | # space. 304 | #epub_theme = 'epub' 305 | 306 | # The language of the text. It defaults to the language option 307 | # or 'en' if the language is not set. 308 | #epub_language = '' 309 | 310 | # The scheme of the identifier. Typical schemes are ISBN or URL. 311 | #epub_scheme = '' 312 | 313 | # The unique identifier of the text. This can be a ISBN number 314 | # or the project homepage. 315 | #epub_identifier = '' 316 | 317 | # A unique identification for the text. 318 | #epub_uid = '' 319 | 320 | # A tuple containing the cover image and cover page html template filenames. 321 | #epub_cover = () 322 | 323 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 324 | #epub_guide = () 325 | 326 | # HTML files that should be inserted before the pages created by sphinx. 327 | # The format is a list of tuples containing the path and title. 328 | #epub_pre_files = [] 329 | 330 | # HTML files shat should be inserted after the pages created by sphinx. 331 | # The format is a list of tuples containing the path and title. 332 | #epub_post_files = [] 333 | 334 | # A list of files that should not be packed into the epub file. 335 | epub_exclude_files = ['search.html'] 336 | 337 | # The depth of the table of contents in toc.ncx. 338 | #epub_tocdepth = 3 339 | 340 | # Allow duplicate toc entries. 341 | #epub_tocdup = True 342 | 343 | # Choose between 'default' and 'includehidden'. 344 | #epub_tocscope = 'default' 345 | 346 | # Fix unsupported image types using the Pillow. 347 | #epub_fix_images = False 348 | 349 | # Scale large images. 350 | #epub_max_image_width = 0 351 | 352 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 353 | #epub_show_urls = 'inline' 354 | 355 | # If false, no index is generated. 356 | #epub_use_index = True 357 | -------------------------------------------------------------------------------- /stoplight/tests/test_validation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import threading 4 | from unittest import TestCase 5 | 6 | import mock 7 | 8 | import stoplight 9 | from stoplight import * 10 | from stoplight.exceptions import * 11 | 12 | VALIDATED_STR = 'validated' 13 | globalctx = threading.local() 14 | 15 | IS_UPPER_REGEX = re.compile('^[A-Z]*$') 16 | 17 | 18 | @validation_function 19 | def is_upper(z): 20 | """Simple validation function for testing purposes 21 | that ensures that input is all caps 22 | """ 23 | if not IS_UPPER_REGEX.match(z): 24 | raise ValidationFailed('{0} is not uppercase'.format(z)) 25 | 26 | 27 | @validation_function 28 | def is_json(candidate): 29 | import json 30 | try: 31 | # Note: this is an example only and probably not 32 | # a good idea to use this in production. Use 33 | # a real json validation framework 34 | json.loads(candidate) 35 | except ValueError: 36 | raise ValidationFailed('Input must be valid json') 37 | 38 | 39 | def is_type(t): 40 | @validation_function 41 | def func(value): 42 | # Make sure the user actually passed us a type 43 | if not isinstance(t, type): 44 | raise TypeError('Type of "type" is required') 45 | 46 | if not isinstance(value, t): 47 | raise ValidationFailed('Input is incorrect type') 48 | return func 49 | 50 | 51 | error_count = 0 52 | 53 | 54 | def abort(code): 55 | global error_count 56 | error_count = error_count + 1 57 | 58 | 59 | detailed_errors = list() 60 | 61 | 62 | def abort_with_details(code, details): 63 | global error_count, detailed_errors 64 | error_count = error_count + 1 65 | 66 | detailed_errors.append(detailed_errors) 67 | 68 | 69 | other_vals = dict() 70 | get_other_val = other_vals.get 71 | 72 | 73 | class DummyRequest(object): 74 | 75 | def __init__(self): 76 | self.headers = { 77 | 'header1': 'headervalue1', 78 | 'X-Position': '32' 79 | } 80 | 81 | self.body = "" 82 | 83 | 84 | class DummyResponse(object): 85 | pass 86 | 87 | 88 | @validation_function 89 | def is_request(candidate): 90 | if not isinstance(candidate, DummyRequest): 91 | raise ValidationFailed('Input must be a request') 92 | 93 | 94 | @validation_function 95 | def is_response(candidate): 96 | if not isinstance(candidate, DummyResponse): 97 | raise ValidationFailed('Input must be a response') 98 | 99 | 100 | ResponseRule = Rule(is_response(), lambda: abort(404)) 101 | UppercaseRule = Rule(is_upper(), lambda: abort(404)) 102 | 103 | 104 | class RequestRule(Rule): 105 | 106 | def __init__(self, *nested_rules): 107 | """Constructs a new Rule for validating requests. Any 108 | nested rules needed for validating parts of the request 109 | (such as headers, query string params, etc) should 110 | also be passed in. 111 | 112 | :param nested_rules: Any sub rules that also should be 113 | used for validation 114 | """ 115 | def _onerror(): 116 | return abort(500) 117 | 118 | # If we get something that's not a request here, 119 | # something bad happened in the server (i.e. 120 | # maybe a programming error), so return a 500 121 | Rule.__init__(self, vfunc=is_request(), 122 | errfunc=_onerror, 123 | nested_rules=list(nested_rules)) 124 | 125 | 126 | class Matryoshka(object): 127 | 128 | def __init__(self, name, inner=None): 129 | 130 | """Constructs a new Matryoshka 131 | 132 | :param name: The name of this Matryoshka 133 | :param inner: The Matryoshka contained therein 134 | """ 135 | self._name = name 136 | self._inner = inner 137 | 138 | @property 139 | def inner(self): 140 | return self._inner 141 | 142 | @property 143 | def name(self): 144 | return self._name 145 | 146 | 147 | # Example showing how to use a closure and a validation 148 | # function simultaneously. 149 | def has_name(name): 150 | @validation_function 151 | def _inner(obj): 152 | if name != obj.name: 153 | msg = "Name '{0}' does not match '{1}" 154 | msg = msg.format(obj.name, name) 155 | raise ValidationFailed(msg) 156 | 157 | return _inner 158 | 159 | 160 | class MatryoshkaRule(Rule): 161 | def __init__(self, name, *nested_rules, **kwargs): 162 | """Constructs a new Matryoshka rule""" 163 | self.matryoshka_name = name 164 | 165 | outer = kwargs.get("outer") 166 | getter = kwargs.get("getter") 167 | 168 | def _onerror(): 169 | return abort(500) 170 | 171 | def _getter(matryoshka): 172 | return matryoshka.inner 173 | 174 | Rule.__init__(self, vfunc=has_name(name)(), 175 | getter=getter if outer else _getter, 176 | errfunc=_onerror, nested_rules=list(nested_rules)) 177 | 178 | 179 | class HeaderRule(Rule): 180 | 181 | def __init__(self, headername, vfunc, errfunc): 182 | def getter(req): 183 | return req.headers.get(headername) 184 | 185 | Rule.__init__(self, vfunc=vfunc, getter=getter, errfunc=errfunc) 186 | 187 | 188 | class BodyRule(Rule): 189 | 190 | def __init__(self, vfunc, errfunc): 191 | def _getter(req): 192 | return req.body 193 | 194 | Rule.__init__(self, vfunc=vfunc, getter=_getter, errfunc=errfunc) 195 | 196 | 197 | PositionRule = HeaderRule("X-Position", is_type(str)(), lambda: abort(404)) 198 | 199 | PositionRuleDetailed = HeaderRule("X-Position", is_type(str)(), 200 | lambda err: abort_with_details(404, err)) 201 | 202 | JsonBodyRule = BodyRule(is_json(empty_ok=True), lambda: abort(404)) 203 | 204 | PositionRuleProgError = HeaderRule( 205 | "X-Position", is_type(str), lambda: abort(404)) 206 | 207 | 208 | def abort_and_raise(msg): 209 | raise RuntimeError(msg) 210 | 211 | 212 | FunctionalUppercaseRule = Rule(is_upper(), 213 | lambda: abort_and_raise('not uppercase')) 214 | 215 | 216 | @validate(value=FunctionalUppercaseRule) 217 | def FunctionValidation(value): 218 | return value 219 | 220 | 221 | class DummyEndpoint(object): 222 | 223 | # This should throw a ValidationProgrammingError 224 | # when called because the user did not actually 225 | # call validate_upper. 226 | 227 | # Note: the lambda in this function can never actually be 228 | # called, so we use no cover here 229 | @validate(value=Rule(is_upper, lambda: abort(404))) # pragma: no cover 230 | def get_value_programming_error(self, value): 231 | # This function body should never be 232 | # callable since the validation error 233 | # should not allow it to be called 234 | assert False # pragma: no cover 235 | 236 | @validate( 237 | value1=Rule(is_upper(), lambda: abort(404)), 238 | value2=Rule(is_upper(), lambda: abort(404)), 239 | value3=Rule(is_upper(), lambda: abort(404)) 240 | ) # pragma: no cover 241 | def get_value_happy_path(self, value1, value2, value3): 242 | return value1 + value2 + value3 243 | 244 | # Falcon-style endpoint 245 | @validate( 246 | request=Rule(is_request(), lambda: abort(404)), 247 | response=Rule(is_response(), lambda: abort(404)), 248 | value=Rule(is_upper(), lambda: abort(404)) 249 | ) 250 | def get_falcon_style(self, request, response, value): 251 | return value 252 | 253 | # Falcon-style w/ delcared rules 254 | @validate(request=RequestRule(), response=ResponseRule, 255 | value=UppercaseRule) 256 | def get_falcon_style2(self, request, response, value): 257 | return value 258 | 259 | # Stoplight allows the user to express rules, alias them, 260 | # then clearly know what is being validated. 261 | @validate( 262 | request=RequestRule(PositionRule, JsonBodyRule), 263 | response=ResponseRule, 264 | value=Rule(is_type(int)(), lambda: abort(400)) 265 | ) 266 | def do_something(self, request, response, value): 267 | return value 268 | 269 | # Use a position rule that has a programming error. 270 | # This should throw 271 | @validate( 272 | request=RequestRule(PositionRuleProgError, JsonBodyRule), 273 | response=ResponseRule, 274 | value=Rule(is_type(int)(), lambda: abort(400)) 275 | ) 276 | def do_something_programming_error(self, request, response, value): 277 | return value 278 | 279 | # Stoplight allows the user to express rules, alias them, 280 | # then clearly know what is being validated. 281 | @validate( 282 | request=RequestRule(PositionRuleDetailed), 283 | response=ResponseRule, 284 | value=Rule(is_type(int)(), lambda err: abort_with_details(400, err)) 285 | ) 286 | def detailed_error_ep(self, request, response, value): 287 | return value 288 | 289 | # Validation function with only global-scopped validations 290 | @validate(Rule(is_upper(), lambda err: abort_with_details(400, err), 291 | lambda z: globalctx.testvalue)) 292 | def do_something_global(self): 293 | return globalctx.testvalue 294 | 295 | @validate(Rule(is_upper(), lambda err: abort_with_details(400, err))) 296 | def free_rule_no_getter(self): 297 | return 298 | 299 | # Test nested rules 300 | @validate(param=MatryoshkaRule("large", 301 | MatryoshkaRule("med", MatryoshkaRule("small")), 302 | outer=True) 303 | ) 304 | def nested_rules(self, param): 305 | return param.name 306 | 307 | # Demonstrates a programming error. Every parameter but 'self' must 308 | # be validated. 309 | @validate(param1=UppercaseRule) 310 | def missing_parameters(self, param1, param2): 311 | return param1 + param2 312 | 313 | # Demonstrates passing a rule for a parameter that doesn't actually 314 | # exist 315 | @validate(param1=UppercaseRule, param2=UppercaseRule, param3=UppercaseRule) 316 | def superfluous_parameter(self, param1, param2): 317 | return param1 + param2 318 | 319 | 320 | class TestValidationFunction(TestCase): 321 | 322 | def test_empty_ok(self): 323 | is_upper(empty_ok=True)('') 324 | 325 | with self.assertRaises(ValidationFailed): 326 | is_upper()('') 327 | 328 | is_upper(none_ok=True)(None) 329 | 330 | with self.assertRaises(ValidationFailed): 331 | is_upper()(None) 332 | 333 | 334 | class TestValidationDecorator(TestCase): 335 | 336 | def setUp(self): 337 | self.ep = DummyEndpoint() 338 | 339 | def test_missing_parameters(self): 340 | with self.assertRaises(ValidationProgrammingError): 341 | self.ep.missing_parameters('value1', 'value2') 342 | 343 | def test_superfluous_parameter(self): 344 | with self.assertRaises(ValidationProgrammingError): 345 | self.ep.superfluous_parameter('value1', 'value2') 346 | 347 | def test_function_style_validation(self): 348 | 349 | positive_cases = [ 350 | 'A', 'B', 'C', 351 | 'AA', 'AB', 'CZ', 352 | 'RED', 'GREEN', 'BLUE', 353 | ] 354 | negative_cases = [ 355 | 'z', 'y', 'z', 356 | 'ww', 'vv', 'uu', 357 | 'serial', 'cereal', 'surreal', 358 | '}', '{', '\\}', '\\{', r'\{', r'\}' 359 | ] 360 | 361 | for case in positive_cases: 362 | self.assertEqual(case, FunctionValidation(case)) 363 | 364 | for case in negative_cases: 365 | with self.assertRaises(RuntimeError): 366 | FunctionValidation(case) 367 | 368 | def test_nested_rules(self): 369 | global error_count 370 | 371 | matryoshka_small = Matryoshka("small") 372 | matryoshka_med = Matryoshka("med", matryoshka_small) 373 | matryoshka_large = Matryoshka("large", matryoshka_med) 374 | 375 | # This should succeed! 376 | oldcount = error_count 377 | out = self.ep.nested_rules(matryoshka_large) 378 | self.assertEqual(oldcount, error_count) 379 | self.assertEqual(matryoshka_large.name, out) 380 | 381 | # This should fail 382 | oldcount = error_count 383 | self.ep.nested_rules(matryoshka_small) 384 | self.assertEqual(oldcount + 1, error_count) 385 | 386 | def test_programming_error(self): 387 | with self.assertRaises(ValidationProgrammingError): 388 | self.ep.get_value_programming_error('AT_ME') 389 | 390 | def test_detailed_errfuncs(self): 391 | global error_count, detailed_errors 392 | 393 | request = DummyRequest() 394 | response = DummyResponse() 395 | 396 | """ 397 | # Should succeed 398 | oldcount = error_count 399 | self.ep.detailed_error_ep(request, response, 1) 400 | self.assertEqual(oldcount, error_count) 401 | 402 | # Should Fail Validation 403 | detailed_errors = [] 404 | oldcount = error_count 405 | self.ep.detailed_error_ep(request, response, 'blah') 406 | self.assertEqual(oldcount + 1, error_count) 407 | self.assertEqual(len(detailed_errors), 1) 408 | """ 409 | 410 | # Should Fail Validation 411 | detailed_errors = [] 412 | oldcount = error_count 413 | request.headers = {'X-Position': 1} 414 | self.ep.detailed_error_ep(request, response, 1) 415 | self.assertEqual(oldcount + 1, error_count) 416 | self.assertEqual(len(detailed_errors), 1) 417 | 418 | def test_callbacks(self): 419 | global error_count 420 | 421 | request = DummyRequest() 422 | response = DummyResponse() 423 | 424 | # Let's register a bad callback that throws. This is to ensure 425 | # that doing so would not prevent other callbacks from 426 | # happening 427 | def bad_cb(x): 428 | raise Exception("Bad callback") 429 | 430 | stoplight.register_callback(bad_cb) 431 | 432 | valfailures = [] 433 | 434 | # Force an error. X-Position is required 435 | # to be a string type, so passing an int 436 | # should force a validation error 437 | request.headers = {'X-Position': 9876} 438 | 439 | oldcount = error_count 440 | self.ep.do_something(request, response, 3) 441 | self.assertEqual(oldcount + 1, error_count) 442 | 443 | # Our callback wasn't registered, so this should 444 | # have resulted no change to response_obj 445 | self.assertEqual(len(valfailures), 0) 446 | 447 | def cb(x): 448 | valfailures.append(x) 449 | 450 | # Now register a callback and try again 451 | stoplight.register_callback(cb) 452 | 453 | oldcount = error_count 454 | self.ep.do_something(request, response, 3) 455 | self.assertEqual(oldcount + 1, error_count) 456 | 457 | self.assertEqual(len(valfailures), 1) 458 | 459 | # Try again, should increment the count again 460 | oldcount = error_count 461 | self.ep.do_something(request, response, 3) 462 | self.assertEqual(oldcount + 1, error_count) 463 | 464 | self.assertEqual(len(valfailures), 2) 465 | stoplight.unregister_callback(cb) 466 | stoplight.unregister_callback(bad_cb) 467 | 468 | # removing a bogus callback should fail silently 469 | stoplight.unregister_callback(lambda x: None) 470 | 471 | # Now, let's get the second item and do some 472 | # validations on it 473 | obj = valfailures[1] 474 | 475 | # Parameter-level stuff 476 | self.assertIsInstance(obj.rule, RequestRule) 477 | self.assertEqual(obj.function.__name__, 'do_something') 478 | self.assertEqual(obj.parameter, 'request') 479 | self.assertIsInstance(obj.parameter_value, DummyRequest) 480 | 481 | # nested level stuff 482 | self.assertIsInstance(obj.nested_failure.rule, HeaderRule) 483 | self.assertEqual(obj.nested_failure.parameter_value, 9876) 484 | 485 | # This is the exception that should have been thrown 486 | self.assertIsInstance(obj.ex, ValidationFailed) 487 | 488 | pretty = str(obj) 489 | 490 | # Let's ensure that some basic information is in there 491 | self.assertIn('RequestRule', pretty) 492 | self.assertIn(obj.function.__name__, pretty) 493 | self.assertIn(obj.parameter, pretty) 494 | self.assertIn(str(obj.nested_failure), pretty) 495 | self.assertIn(str(obj.ex), pretty) 496 | 497 | def test_falcon_style(self): 498 | 499 | global error_count 500 | 501 | request = DummyRequest() 502 | response = DummyResponse() 503 | 504 | # Try to call with missing params. The validation 505 | # function should never get called 506 | with self.assertRaises(ValidationProgrammingError) as ctx: 507 | self.ep.get_falcon_style(response, 'HELLO') 508 | 509 | # Try to pass a string to a positional argument 510 | # where a response is expected 511 | oldcount = error_count 512 | self.ep.get_falcon_style(request, "bogusinput", 'HELLO') 513 | self.assertEqual(oldcount + 1, error_count) 514 | 515 | # Pass in as kwargs with good input but out of 516 | # typical order (should succeed) 517 | oldcount = error_count 518 | self.ep.get_falcon_style(response=response, value='HELLO', 519 | request=request) 520 | self.assertEqual(oldcount, error_count) 521 | 522 | # Pass in as kwvalues with good input but out of 523 | # typical order with an invalid value (lower-case 'h') 524 | oldcount = error_count 525 | self.ep.get_falcon_style(response=response, value='hELLO', 526 | request=request) 527 | self.assertEqual(oldcount + 1, error_count) 528 | 529 | # Pass in as kwvalues with good input but out of typical order 530 | # and pass an invalid value. Note that here the response is 531 | # assigned to request, etc. 532 | oldcount = error_count 533 | self.ep.get_falcon_style(response=request, value='HELLO', 534 | request=response) 535 | self.assertEqual(oldcount + 1, error_count) 536 | 537 | # Happy path 538 | oldcount = error_count 539 | self.ep.get_falcon_style(request, response, 'HELLO') 540 | self.assertEqual(oldcount, error_count) 541 | 542 | # This should fail because 3 should be a str, not 543 | # an int 544 | oldcount = error_count 545 | self.ep.do_something(request, response, '3') 546 | self.assertEqual(oldcount + 1, error_count) 547 | 548 | # This should now be successful 549 | oldcount = error_count 550 | self.ep.do_something(request, response, 3) 551 | self.assertEqual(oldcount, error_count) 552 | 553 | # Now change the request so that the body is 554 | # something not considered valid json. This should 555 | # cause a failure of the nested error 556 | request.body = "{" 557 | oldcount = error_count 558 | self.ep.do_something(request, response, 3) 559 | self.assertEqual(oldcount + 1, error_count) 560 | 561 | # Switch request back to normal. Should succeed 562 | request.body = "" 563 | oldcount = error_count 564 | self.ep.do_something(request, response, 3) 565 | self.assertEqual(oldcount, error_count) 566 | 567 | # Now try one with a programming erro 568 | oldcount = error_count 569 | 570 | with self.assertRaises(ValidationProgrammingError) as ctx: 571 | self.ep.do_something_programming_error(request, response, 3) 572 | 573 | self.assertEqual(oldcount, error_count) 574 | 575 | def test_falcon_style_decld_rules(self): 576 | # The following tests repeat the above 577 | # tests, but this time they test using the 578 | # endpoint with the rules being declared 579 | # separately. See get_falcon_style2 above 580 | 581 | global error_count 582 | 583 | request = DummyRequest() 584 | response = DummyResponse() 585 | 586 | # Try to call with missing params. The validation 587 | # function should never get called 588 | with self.assertRaises(ValidationProgrammingError) as ctx: 589 | self.ep.get_falcon_style2(response, 'HELLO') 590 | 591 | # Try to pass a string to a positional argument 592 | # where a response is expected 593 | oldcount = error_count 594 | self.ep.get_falcon_style2(request, "bogusinput", 'HELLO') 595 | self.assertEqual(oldcount + 1, error_count) 596 | 597 | # Pass in as kwvalues with good input but out of 598 | # typical order (should succeed) 599 | oldcount = error_count 600 | self.ep.get_falcon_style2(response=response, value='HELLO', 601 | request=request) 602 | self.assertEqual(oldcount, error_count) 603 | 604 | # Pass in as kwvalues with good input but out of 605 | # typical order with an invalid value (lower-case 'h') 606 | oldcount = error_count 607 | self.ep.get_falcon_style2(response=response, value='hELLO', 608 | request=request) 609 | self.assertEqual(oldcount + 1, error_count) 610 | 611 | # Pass in as kwvalues with good input but out of typical order 612 | # and pass an invalid value. Note that here the response is 613 | # assigned to request, etc. 614 | oldcount = error_count 615 | self.ep.get_falcon_style2(response=request, value='HELLO', 616 | request=response) 617 | self.assertEqual(oldcount + 1, error_count) 618 | 619 | # Happy path 620 | oldcount = error_count 621 | self.ep.get_falcon_style2(request, response, 'HELLO') 622 | self.assertEqual(oldcount, error_count) 623 | 624 | def test_happy_path_and_validation_failure(self): 625 | global error_count 626 | # Should not throw 627 | res = self.ep.get_value_happy_path('WHATEVER', 'HELLO', 'YES') 628 | self.assertEqual('WHATEVERHELLOYES', res) 629 | 630 | # Validation should have failed, and 631 | # we should have seen a tick in the error count 632 | oldcount = error_count 633 | res = self.ep.get_value_happy_path('WHAtEVER', 'HELLO', 'YES') 634 | self.assertEqual(oldcount + 1, error_count) 635 | 636 | # Check passing a None value. This decorator does 637 | # not permit none values. 638 | oldcount = error_count 639 | res = self.ep.get_value_happy_path(None, 'HELLO', 'YES') 640 | self.assertEqual(oldcount + 1, error_count) 641 | 642 | def test_global_ctx(self): 643 | global globalctx 644 | global error_count 645 | 646 | globalctx.testvalue = "SOMETHING" # Should succeed 647 | res = self.ep.do_something_global() 648 | self.assertEqual(globalctx.testvalue, "SOMETHING") 649 | 650 | oldcount = error_count 651 | globalctx.testvalue = "Something" # Should succeed 652 | res = self.ep.do_something_global() 653 | self.assertEqual(oldcount + 1, error_count) 654 | 655 | def test_free_rule_no_getter(self): 656 | with self.assertRaises(ValidationProgrammingError): 657 | res = self.ep.free_rule_no_getter() 658 | 659 | def test_validation_failure_deprecation_warning(self): 660 | with mock.patch('warnings.warn') as mock_warning: 661 | ValidationFailed('hello {0}', 'world') 662 | self.assertEqual(mock_warning.call_count, 1) 663 | 664 | def test_validation_programmering_error_deprecation_warning(self): 665 | with mock.patch('warnings.warn') as mock_warning: 666 | ValidationProgrammingError('hello {0}', 'world') 667 | self.assertEqual(mock_warning.call_count, 1) 668 | --------------------------------------------------------------------------------