Skip to content
This repository was archived by the owner on Mar 18, 2022. It is now read-only.

Extend support for Python version validation #21

Merged
merged 2 commits into from
Feb 9, 2017
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
14 changes: 7 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
language: python
python:
- 2.7
- 2.7
sudo: false
env:
- TOX_ENV=py27-unittest
- TOX_ENV=py27-integration
- TOX_ENV=lint
#- TOX_ENV=docs
- TOX_ENV=py27-unittest
- TOX_ENV=py27-integration
- TOX_ENV=lint
#- TOX_ENV=docs
install:
- pip install tox
- pip install tox
script:
- tox -e $TOX_ENV
- tox -e $TOX_ENV
notifications:
slack:
rooms:
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This is a fine set of docs
:glob:

steps
spec



Expand Down
13 changes: 13 additions & 0 deletions spec.rst → docs/spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ Following mapping keys are supported (all but the marked once are optional):
The path in which ``python setup.py install`` will be executed.
Defaults to the repository root.

``version``
Copy link
Member

Choose a reason for hiding this comment

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

Seems weird there's an example here that doesn't include the actual version object that it's documenting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This could be moved into docs on the actual config object, as it's not important for the spec. None of the elements have an example currently, so I don't think one is necessarily warranted here yet. I could see overhauling this doc to include more information eventually however.

The Python interpreter version to use for all build calls. This value
should be a float or integer value.

Supported versions can be configured on config instantiation by passing
in the following to the `env_config`::

{
'python': {
'supported_versions': [2, 2.7, 3, 3.5],
}
}

``language``
The language the doc is written in. Defaults to empty string.

Expand Down
2 changes: 2 additions & 0 deletions readthedocs.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
name: read-the-docs
type: sphinx
python:
version: 3.6
28 changes: 20 additions & 8 deletions readthedocs_build/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ class BuildConfig(dict):
PYTHON_EXTRA_REQUIREMENTS_INVALID_MESSAGE = (
'"python.extra_requirements" section must be a list.')

PYTHON_SUPPORTED_VERSIONS = [2, 2.7, 3, 3.3, 3.4, 3.5, 3.6]
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to support <3.5?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this can be agnostic to what versions we actually support, this is just checking for valid version numbers ATM. This decision should be left up to our implementation on RTD, i think.


def __init__(self, env_config, raw_config, source_file, source_position):
self.env_config = env_config
self.raw_config = raw_config
Expand Down Expand Up @@ -113,13 +115,11 @@ def get_valid_types(self):
)

def get_valid_python_versions(self):
return (
2,
2.7,
3,
3.4,
3.5,
)
try:
return self.env_config['python']['supported_versions']
except (KeyError, TypeError):
pass
return self.PYTHON_SUPPORTED_VERSIONS

def get_valid_formats(self):
return (
Expand Down Expand Up @@ -266,8 +266,20 @@ def validate_python(self):

if 'version' in raw_python:
with self.catch_validation_error('python.version'):
# Try to convert strings to an int first, to catch '2', then
# a float, to catch '2.7'
version = raw_python['version']
if isinstance(version, str):
try:
version = int(version)
except ValueError:
try:
version = float(version)
except ValueError:
pass
python['version'] = validate_choice(
raw_python['version'], self.get_valid_python_versions()
version,
self.get_valid_python_versions()
)

self['python'] = python
Expand Down
53 changes: 53 additions & 0 deletions readthedocs_build/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,59 @@ def it_uses_validate_bool(validate_bool):
validate_bool.assert_any_call('to-validate')


def describe_validate_python_version():

def it_defaults_to_a_valid_version():
build = get_build_config({'python': {}})
build.validate_python()
assert build['python']['version'] is 2

def it_supports_other_versions():
build = get_build_config({'python': {'version': 3.6}})
build.validate_python()
assert build['python']['version'] is 3.6

def it_validates_versions_out_of_range():
build = get_build_config({'python': {'version': 1.0}})
with raises(InvalidConfig) as excinfo:
build.validate_python()
assert excinfo.value.key == 'python.version'
assert excinfo.value.code == INVALID_CHOICE

def it_validates_wrong_type():
build = get_build_config({'python': {'version': 'this-is-string'}})
with raises(InvalidConfig) as excinfo:
build.validate_python()
assert excinfo.value.key == 'python.version'
assert excinfo.value.code == INVALID_CHOICE

def it_validates_wrong_type_right_value():
build = get_build_config({'python': {'version': '3.6'}})
build.validate_python()
assert build['python']['version'] == 3.6

build = get_build_config({'python': {'version': '3'}})
build.validate_python()
assert build['python']['version'] == 3

def it_validates_env_supported_versions():
build = get_build_config(
{'python': {'version': 3.6}},
env_config={'python': {'supported_versions': [3.5]}}
)
with raises(InvalidConfig) as excinfo:
build.validate_python()
assert excinfo.value.key == 'python.version'
assert excinfo.value.code == INVALID_CHOICE

build = get_build_config(
{'python': {'version': 3.6}},
env_config={'python': {'supported_versions': [3.5, 3.6]}}
)
build.validate_python()
assert build['python']['version'] == 3.6


def describe_validate_formats():

def it_defaults_to_not_being_included():
Expand Down
32 changes: 30 additions & 2 deletions readthedocs_build/config/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

from .validation import validate_bool
from .validation import validate_choice
from .validation import validate_list
from .validation import validate_directory
from .validation import validate_file
from .validation import validate_path
from .validation import validate_string
from .validation import ValidationError
from .validation import INVALID_BOOL
from .validation import INVALID_CHOICE
from .validation import INVALID_LIST
from .validation import INVALID_DIRECTORY
from .validation import INVALID_FILE
from .validation import INVALID_PATH
Expand Down Expand Up @@ -43,15 +45,41 @@ def it_accepts_valid_choice():
result = validate_choice('choice', ('choice', 'another_choice'))
assert result is 'choice'

result = validate_choice('c', 'abc')
assert result is 'c'
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):
Expand Down
13 changes: 12 additions & 1 deletion readthedocs_build/config/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

INVALID_BOOL = 'invalid-bool'
INVALID_CHOICE = 'invalid-choice'
INVALID_LIST = 'invalid-list'
INVALID_DIRECTORY = 'invalid-directory'
INVALID_FILE = 'invalid-file'
INVALID_PATH = 'invalid-path'
Expand All @@ -17,6 +18,7 @@ class ValidationError(Exception):
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):
Expand All @@ -31,10 +33,19 @@ def __init__(self, value, code, format_kwargs=None):
super(ValidationError, self).__init__(message)


def validate_list(value):
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):
choices = validate_list(choices)
if value not in choices:
raise ValidationError(value, INVALID_CHOICE, {
'choices': ', '.join(choices)
'choices': ', '.join(map(str, choices))
})
return value

Expand Down