1
0
mirror of https://github.com/Zulko/moviepy.git synced 2021-07-27 01:17:47 +03:00

Add tests to prevent decorators arguments inconsistency (#1610)

* Add tests to prevent decorators arguments inconsistency
This commit is contained in:
Álvaro Mondéjar
2021-06-02 14:44:54 +02:00
committed by GitHub
parent 3dc3b4fc9e
commit 215cab6808
3 changed files with 138 additions and 6 deletions

View File

@@ -13,7 +13,6 @@ __all__ = (
"audio_delay",
"audio_fadein",
"audio_fadeout",
"audio_left_right",
"audio_loop",
"audio_normalize",
"multiply_stereo_volume",

View File

@@ -1,8 +1,13 @@
"""Define general test helper attributes and utilities."""
import ast
import contextlib
import functools
import http.server
import importlib
import inspect
import io
import pkgutil
import socketserver
import sys
import tempfile
@@ -52,10 +57,85 @@ def get_mono_wave(freq=440):
@contextlib.contextmanager
def static_files_server(port=8000):
class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
pass
my_server = socketserver.TCPServer(("", port), MyHttpRequestHandler)
my_server = socketserver.TCPServer(("", port), http.server.SimpleHTTPRequestHandler)
thread = threading.Thread(target=my_server.serve_forever, daemon=True)
thread.start()
yield thread
@functools.lru_cache(maxsize=None)
def get_moviepy_modules():
"""Get all moviepy module names and if each one is a package."""
response = []
with contextlib.redirect_stdout(io.StringIO()):
moviepy_module = importlib.import_module("moviepy")
modules = pkgutil.walk_packages(
path=moviepy_module.__path__,
prefix=moviepy_module.__name__ + ".",
)
for importer, modname, ispkg in modules:
response.append((modname, ispkg))
return response
def get_functions_with_decorator_defined(code, decorator_name):
"""Get all functions in a code object which have a decorator defined,
along with the arguments of the function and the decorator.
Parameters
----------
code : object
Module or class object from which to retrieve the functions.
decorator_name : str
Name of the decorator defined in the functions to search.
"""
class FunctionsWithDefinedDecoratorExtractor(ast.NodeVisitor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.functions_with_decorator = []
def generic_visit(self, node):
if isinstance(node, ast.FunctionDef) and node.decorator_list:
for dec in node.decorator_list:
if not isinstance(dec, ast.Call) or dec.func.id != decorator_name:
continue
decorator_argument_names = []
if isinstance(dec.args, ast.List):
for args in dec.args:
decorator_argument_names.extend(
[e.value for e in args.elts]
)
else:
for args in dec.args:
if isinstance(args, (ast.List, ast.Tuple)):
decorator_argument_names.extend(
[e.value for e in args.elts]
)
else:
decorator_argument_names.append(args.value)
function_argument_names = [arg.arg for arg in node.args.args]
for arg in node.args.kwonlyargs:
function_argument_names.append(arg.arg)
self.functions_with_decorator.append(
{
"function_name": node.name,
"function_arguments": function_argument_names,
"decorator_arguments": decorator_argument_names,
}
)
ast.NodeVisitor.generic_visit(self, node)
modtree = ast.parse(inspect.getsource(code))
visitor = FunctionsWithDefinedDecoratorExtractor()
visitor.visit(modtree)
return visitor.functions_with_decorator

View File

@@ -13,7 +13,12 @@ import pytest
import moviepy.tools as tools
from moviepy.video.io.downloader import download_webfile
from tests.test_helper import TMP_DIR, static_files_server
from tests.test_helper import (
TMP_DIR,
get_functions_with_decorator_defined,
get_moviepy_modules,
static_files_server,
)
@pytest.mark.parametrize(
@@ -274,5 +279,53 @@ def test_config_check():
del sys.modules["moviepy.config"]
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8 or greater")
@pytest.mark.parametrize(
"decorator_name",
("convert_parameter_to_seconds", "convert_path_to_string"),
)
def test_decorators_argument_converters_consistency(decorator_name):
"""Checks that for all functions that have a decorator defined (like
``@convert_parameter_to_seconds``), the parameters passed to the decorator
correspond to the parameters taken by the function.
This test is util to prevent next case in which the parameter names doesn't
match between the decorator and the function definition:
>>> @convert_parameter_to_seconds(['foo']) # doctest: +SKIP
>>> def whatever_function(bar): # bar not converted to seconds
... pass
Some wrong defintions remained unnoticed in the past before this test was
added.
"""
with contextlib.redirect_stdout(io.StringIO()):
for modname, ispkg in get_moviepy_modules():
if ispkg:
continue
try:
module = importlib.import_module(modname)
except ImportError:
continue
functions_with_decorator = get_functions_with_decorator_defined(
module,
decorator_name,
)
for function_data in functions_with_decorator:
for argument_name in function_data["decorator_arguments"]:
funcname = function_data["function_name"]
assert argument_name in function_data["function_arguments"], (
f"Wrong argument name '{argument_name}' in"
f" '@{decorator_name}' decorator for function"
f" '{funcname}' found inside module '{modname}'"
)
assert function_data["decorator_arguments"]
assert function_data["function_arguments"]
if __name__ == "__main__":
pytest.main()