Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move config module from rtd-build repo #4242

Merged
merged 7 commits into from
Jun 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest-describe is incompatible with the current pytest (3.6.1) had to downgrade to 3.2.5 😧. We can port those tests later

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make an issue to address this later in the project.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened an issue here #4270

pytest-django==3.1.2
pytest-describe==0.11.0
pytest-xdist==1.22.0
apipkg==1.4
execnet==1.5.0
Expand Down