Skip to content

Commit

Permalink
Move config module from rtd-build repo (#4242)
Browse files Browse the repository at this point in the history
* Add pytest-describe dependencie

* Import tests from validation.py

* Downgrade pytest to 3.2.5

pytest-describe is incompatible with the current pytest (3.6.1)
had to downgrade to 3.2.5

* Move validation.py

* Fix tests

Change the pathed module

* Linter

* Change import
  • Loading branch information
stsewd authored and agjohnson committed Jun 19, 2018
1 parent 017f581 commit db8f092
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 2 deletions.
161 changes: 161 additions & 0 deletions readthedocs/config/tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
from __future__ import division, print_function, unicode_literals

import os

from mock import patch
from pytest import raises
from six import text_type

from readthedocs.config.validation import (
INVALID_BOOL, INVALID_CHOICE, INVALID_DIRECTORY, INVALID_FILE, INVALID_LIST,
INVALID_PATH, INVALID_STRING, ValidationError, validate_bool,
validate_choice, validate_directory, validate_file, validate_list,
validate_path, validate_string)


def describe_validate_bool():
def it_accepts_true():
assert validate_bool(True) is True

def it_accepts_false():
assert validate_bool(False) is False

def it_accepts_0():
assert validate_bool(0) is False

def it_accepts_1():
assert validate_bool(1) is True

def it_fails_on_string():
with raises(ValidationError) as excinfo:
validate_bool('random string')
assert excinfo.value.code == INVALID_BOOL


def describe_validate_choice():

def it_accepts_valid_choice():
result = validate_choice('choice', ('choice', 'another_choice'))
assert result is 'choice'

with raises(ValidationError) as excinfo:
validate_choice('c', 'abc')
assert excinfo.value.code == INVALID_LIST

def it_rejects_invalid_choice():
with raises(ValidationError) as excinfo:
validate_choice('not-a-choice', ('choice', 'another_choice'))
assert excinfo.value.code == INVALID_CHOICE


def describe_validate_list():

def it_accepts_list_types():
result = validate_list(['choice', 'another_choice'])
assert result == ['choice', 'another_choice']

result = validate_list(('choice', 'another_choice'))
assert result == ['choice', 'another_choice']

def iterator():
yield 'choice'

result = validate_list(iterator())
assert result == ['choice']

with raises(ValidationError) as excinfo:
validate_choice('c', 'abc')
assert excinfo.value.code == INVALID_LIST

def it_rejects_string_types():
with raises(ValidationError) as excinfo:
result = validate_list('choice')
assert excinfo.value.code == INVALID_LIST


def describe_validate_directory():

def it_uses_validate_path(tmpdir):
patcher = patch('readthedocs.config.validation.validate_path')
with patcher as validate_path:
path = text_type(tmpdir.mkdir('a directory'))
validate_path.return_value = path
validate_directory(path, str(tmpdir))
validate_path.assert_called_with(path, str(tmpdir))

def it_rejects_files(tmpdir):
tmpdir.join('file').write('content')
with raises(ValidationError) as excinfo:
validate_directory('file', str(tmpdir))
assert excinfo.value.code == INVALID_DIRECTORY


def describe_validate_file():

def it_uses_validate_path(tmpdir):
patcher = patch('readthedocs.config.validation.validate_path')
with patcher as validate_path:
path = tmpdir.join('a file')
path.write('content')
path = str(path)
validate_path.return_value = path
validate_file(path, str(tmpdir))
validate_path.assert_called_with(path, str(tmpdir))

def it_rejects_directories(tmpdir):
tmpdir.mkdir('directory')
with raises(ValidationError) as excinfo:
validate_file('directory', str(tmpdir))
assert excinfo.value.code == INVALID_FILE


def describe_validate_path():

def it_accepts_relative_path(tmpdir):
tmpdir.mkdir('a directory')
validate_path('a directory', str(tmpdir))

def it_accepts_files(tmpdir):
tmpdir.join('file').write('content')
validate_path('file', str(tmpdir))

def it_accepts_absolute_path(tmpdir):
path = str(tmpdir.mkdir('a directory'))
validate_path(path, 'does not matter')

def it_returns_absolute_path(tmpdir):
tmpdir.mkdir('a directory')
path = validate_path('a directory', str(tmpdir))
assert path == os.path.abspath(path)

def it_only_accepts_strings():
with raises(ValidationError) as excinfo:
validate_path(None, '')
assert excinfo.value.code == INVALID_STRING

def it_rejects_non_existent_path(tmpdir):
with raises(ValidationError) as excinfo:
validate_path('does not exist', str(tmpdir))
assert excinfo.value.code == INVALID_PATH


def describe_validate_string():

def it_accepts_unicode():
result = validate_string(u'Unicöde')
assert isinstance(result, text_type)

def it_accepts_nonunicode():
result = validate_string('Unicode')
assert isinstance(result, text_type)

def it_rejects_float():
with raises(ValidationError) as excinfo:
validate_string(123.456)
assert excinfo.value.code == INVALID_STRING

def it_rejects_none():
with raises(ValidationError) as excinfo:
validate_string(None)
assert excinfo.value.code == INVALID_STRING
99 changes: 99 additions & 0 deletions readthedocs/config/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Validations for the RTD configuration file."""
from __future__ import division, print_function, unicode_literals

import os

from six import string_types, text_type

INVALID_BOOL = 'invalid-bool'
INVALID_CHOICE = 'invalid-choice'
INVALID_LIST = 'invalid-list'
INVALID_DIRECTORY = 'invalid-directory'
INVALID_FILE = 'invalid-file'
INVALID_PATH = 'invalid-path'
INVALID_STRING = 'invalid-string'


class ValidationError(Exception):

"""Base error for validations."""

messages = {
INVALID_BOOL: 'expected one of (0, 1, true, false), got {value}',
INVALID_CHOICE: 'expected one of ({choices}), got {value}',
INVALID_DIRECTORY: '{value} is not a directory',
INVALID_FILE: '{value} is not a file',
INVALID_PATH: 'path {value} does not exist',
INVALID_STRING: 'expected string',
INVALID_LIST: 'expected list',
}

def __init__(self, value, code, format_kwargs=None):
self.value = value
self.code = code
defaults = {
'value': value,
}
if format_kwargs is not None:
defaults.update(format_kwargs)
message = self.messages[code].format(**defaults)
super(ValidationError, self).__init__(message)


def validate_list(value):
"""Check if ``value`` is an iterable."""
if isinstance(value, str):
raise ValidationError(value, INVALID_LIST)
if not hasattr(value, '__iter__'):
raise ValidationError(value, INVALID_LIST)
return list(value)


def validate_choice(value, choices):
"""Check that ``value`` is in ``choices``."""
choices = validate_list(choices)
if value not in choices:
raise ValidationError(value, INVALID_CHOICE, {
'choices': ', '.join(map(str, choices))
})
return value


def validate_bool(value):
"""Check that ``value`` is an boolean value."""
if value not in (0, 1, False, True):
raise ValidationError(value, INVALID_BOOL)
return bool(value)


def validate_directory(value, base_path):
"""Check that ``value`` is a directory."""
path = validate_path(value, base_path)
if not os.path.isdir(path):
raise ValidationError(value, INVALID_DIRECTORY)
return path


def validate_file(value, base_path):
"""Check that ``value`` is a file."""
path = validate_path(value, base_path)
if not os.path.isfile(path):
raise ValidationError(value, INVALID_FILE)
return path


def validate_path(value, base_path):
"""Check that ``value`` is an existent file in ``base_path``."""
string_value = validate_string(value)
pathed_value = os.path.join(base_path, string_value)
final_value = os.path.abspath(pathed_value)
if not os.path.exists(final_value):
raise ValidationError(value, INVALID_PATH)
return final_value


def validate_string(value):
"""Check that ``value`` is a string type."""
if not isinstance(value, string_types):
raise ValidationError(value, INVALID_STRING)
return text_type(value)
2 changes: 1 addition & 1 deletion readthedocs/rtd_tests/tests/test_build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest
import six
import yamale
from readthedocs_build.testing import utils
from readthedocs.config.tests import utils
from yamale.validators import DefaultValidators, Validator

V2_SCHEMA = path.join(
Expand Down
5 changes: 4 additions & 1 deletion requirements/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

django-dynamic-fixture==2.0.0

pytest<4,>=3.3.2
# 3.6.1 and >3.2.5 is incompatible
# with pytest-describe 0.11.0
pytest==3.2.5
pytest-django==3.1.2
pytest-describe==0.11.0
pytest-xdist==1.22.0
apipkg==1.4
execnet==1.5.0
Expand Down

0 comments on commit db8f092

Please sign in to comment.