Repository URL to install this package:
|
Version:
3.13.0 ▾
|
import inspect
import os
from functools import wraps
from inspect import Parameter
import mock
from doctor.routing import get_params_from_func
from doctor.utils import (
add_param_annotations, get_description_lines, get_module_attr,
get_valid_class_name, Params, RequestParamAnnotation)
from .base import TestCase
from .types import Age, Auth, Foo, IsAlive, IsDeleted, Name
def does_nothing(func):
"""An example decorator that does nothing."""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def dec_with_args(foo='foo', bar='bar'):
"""An example decorator that takes args and passes value to func."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func('arg', *args, **kwargs)
return wrapper
return decorator
def dec_with_args_one_of_which_allows_a_func(foo, bar=None):
"""An example decorator that takes args where one is a function.
In this case, bar accepts a func which transforms foo.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if bar is not None:
bar(foo)
return func(*args, **kwargs)
return wrapper
return decorator
def no_params() -> Foo:
return ''
def get_foo(name: Name, age: Age, is_alive: IsAlive=True) -> Foo:
return ''
def pass_pos_param(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func('extra!', *args, **kwargs)
return wrapper
@pass_pos_param
def decorated_func(extra: str, name: Name, is_alive: IsAlive=True) -> Foo:
return ''
class TestUtils(TestCase):
def test_add_param_annotations(self):
new_params = [
RequestParamAnnotation('auth', Auth, required=True),
RequestParamAnnotation('is_deleted', IsDeleted)
]
actual = add_param_annotations(get_foo, new_params)
# auth and is_deleted should be added to `all`.
# auth should be added to `required`.
# is_deleted should be added to `optional`.
# `logic` should be unmodified.
expected_params = Params(
all=['name', 'age', 'is_alive', 'auth', 'is_deleted'],
logic=['name', 'age', 'is_alive'],
required=['name', 'age', 'auth'],
optional=['is_alive', 'is_deleted']
)
assert expected_params == actual._doctor_params
# verify `auth` added to the doctor_signature.
expected = Parameter('auth', Parameter.KEYWORD_ONLY,
default=Parameter.empty, annotation=Auth)
auth = actual._doctor_signature.parameters['auth']
assert expected == auth
# verify `is_deleted` added to the doctor signature.
expected = Parameter('is_deleted', Parameter.KEYWORD_ONLY,
default=None, annotation=IsDeleted)
is_deleted = actual._doctor_signature.parameters['is_deleted']
assert expected == is_deleted
def test_add_param_annotations_mutliple_calls(self):
"""
This test verifies if we call this function multiple times with
the same logic function that it doesn't squash parameters added
from the previous call. This is a regression test.
"""
delattr(get_foo, '_doctor_signature')
new_params = [
RequestParamAnnotation('auth', Auth, required=True),
]
actual = add_param_annotations(get_foo, new_params)
expected_params = Params(
all=['name', 'age', 'is_alive', 'auth'],
logic=['name', 'age', 'is_alive'],
required=['name', 'age', 'auth'],
optional=['is_alive']
)
assert expected_params == actual._doctor_params
# verify `auth` added to the doctor_signature.
expected = Parameter('auth', Parameter.KEYWORD_ONLY,
default=Parameter.empty, annotation=Auth)
auth = actual._doctor_signature.parameters['auth']
assert expected == auth
# Now call again adding the is_deleted param. It should add to and
# not replace the auth param we added previously.
new_params = [
RequestParamAnnotation('is_deleted', IsDeleted)
]
actual = add_param_annotations(get_foo, new_params)
expected_params = Params(
all=['name', 'age', 'is_alive', 'auth', 'is_deleted'],
logic=['name', 'age', 'is_alive'],
required=['name', 'age', 'auth'],
optional=['is_alive', 'is_deleted']
)
assert expected_params == actual._doctor_params
# verify `is_deleted` added to the doctor signature.
expected = Parameter('is_deleted', Parameter.KEYWORD_ONLY,
default=None, annotation=IsDeleted)
is_deleted = actual._doctor_signature.parameters['is_deleted']
assert expected == is_deleted
@mock.patch('doctor.utils.logging')
def test_add_param_annotations_duplicate_param(self, mock_logging):
delattr(get_foo, '_doctor_signature')
new_params = [
# name param is already in `get_foo` signature
RequestParamAnnotation('name', Name, required=True),
RequestParamAnnotation('is_deleted', IsDeleted)
]
actual = add_param_annotations(get_foo, new_params)
# verify `is_deleted` added to the doctor signature.
expected = Parameter('is_deleted', Parameter.KEYWORD_ONLY,
default=None, annotation=IsDeleted)
is_deleted = actual._doctor_signature.parameters['is_deleted']
assert expected == is_deleted
# verify a warning was logged.
expected_call = mock.call(
'Not adding %s to signature of %s, function already has that '
'parameter in its signature.', 'name', 'get_foo')
assert expected_call == mock_logging.warning.call_args
@mock.patch('doctor.utils.open',
new_callable=mock.mock_open,
read_data='mock_attr = "something"')
@mock.patch('doctor.utils.compile', create=True)
@mock.patch('doctor.utils.os', autospec=True)
@mock.patch('doctor.utils.sys', autospec=True)
def test_get_module_attr(self, mock_sys, mock_os, mock_compile, m):
def side_effect(source, filename, mode, flags=0, dont_inherit=False):
assert mock_sys.path == ['foo', 'bar', '/foo/bar']
return compile(source, filename, mode, flags=flags,
dont_inherit=dont_inherit)
mock_compile.side_effect = side_effect
mock_os.getcwd.return_value = mock.sentinel.old_cwd
mock_os.path = os.path
mock_sys.path = ['foo', 'bar']
namespace = {}
result = get_module_attr('/foo/bar/baz', 'mock_attr',
namespace=namespace)
m.assert_called_once_with('/foo/bar/baz', 'r')
mock_compile.assert_called_once_with('mock_attr = "something"',
'/foo/bar/baz', 'exec')
assert result == 'something'
assert namespace['__file__'] == '/foo/bar/baz'
assert mock_os.chdir.call_args_list == [
mock.call('/foo/bar'),
mock.call(mock.sentinel.old_cwd),
]
assert mock_sys.path == ['foo', 'bar']
def test_get_description_lines(self):
"""
Tests that get_description_lines properly dedents docstrings and strips
out values we don't want.
"""
docstring = """line one
line two
indented line three
line four
:param str c: example param
:returns: d
"""
assert get_description_lines(docstring) == [
'line one',
'',
'line two',
'',
' indented line three',
'',
'line four',
'',
]
def test_get_description_lines_none(self):
"""It should just return an empty list for None."""
assert get_description_lines(None) == []
def test_get_description_lines_trailing_newline(self):
"""It should add a trailing line if necessary."""
assert get_description_lines('foo\n:arg') == ['foo', '']
def test_get_params_from_func(self):
get_foo._doctor_signature = inspect.signature(get_foo)
expected = Params(
all=['name', 'age', 'is_alive'],
optional=['is_alive'],
required=['name', 'age'],
logic=['name', 'age', 'is_alive'])
assert expected == get_params_from_func(get_foo)
def test_get_params_from_func_no_params(self):
# no signature passed or defined on the function
expected = Params([], [], [], [])
assert expected == get_params_from_func(no_params)
# signature passed in
signature = inspect.signature(no_params)
assert expected == get_params_from_func(no_params, signature)
# signature attached to logic function
no_params._doctor_signature = inspect.signature(no_params)
assert expected == get_params_from_func(no_params)
def test_get_params_from_func_decorated_func(self):
"""
Verifies that we don't include the `extra` param as required since
it's not a sublcass of `SuperType` and is passed to the function
by a decorator.
"""
decorated_func._doctor_signature = inspect.signature(decorated_func)
expected = Params(
all=['extra', 'name', 'is_alive'],
required=['name'],
optional=['is_alive'],
logic=['extra', 'name', 'is_alive'])
assert expected == get_params_from_func(decorated_func)
def test_get_valid_class_name(self):
tests = (
# (input, expected)
('Notes', 'Notes'),
('Notes (v1)', 'NotesV1'),
('Notes - "v1"', 'NotesV1'),
('note_book.', 'NoteBook'),
('notes', 'Notes'),
('note-book_v1 .', 'NoteBookV1'),
)
for s, expected in tests:
assert expected == get_valid_class_name(s)