Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
Tests for documentation (#207)
Browse files Browse the repository at this point in the history
* Add documentation test

* fix dtype

* Comments
  • Loading branch information
Frédéric Branchaud-Charron authored and rragundez committed May 5, 2019
1 parent d40cbb0 commit 0494094
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 9 deletions.
13 changes: 8 additions & 5 deletions keras_preprocessing/image/affine_transformations.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ def random_rotation(x, rg, row_axis=1, col_axis=2, channel_axis=0,
(one of `{'constant', 'nearest', 'reflect', 'wrap'}`).
cval: Value used for points outside the boundaries
of the input if `mode='constant'`.
interpolation_order int: order of spline interpolation.
interpolation_order: int, order of spline interpolation.
see `ndimage.interpolation.affine_transform`
# Returns
Rotated Numpy image tensor.
"""
Expand All @@ -74,8 +75,9 @@ def random_shift(x, wrg, hrg, row_axis=1, col_axis=2, channel_axis=0,
(one of `{'constant', 'nearest', 'reflect', 'wrap'}`).
cval: Value used for points outside the boundaries
of the input if `mode='constant'`.
interpolation_order int: order of spline interpolation.
interpolation_order: int, order of spline interpolation.
see `ndimage.interpolation.affine_transform`
# Returns
Shifted Numpy image tensor.
"""
Expand Down Expand Up @@ -103,8 +105,9 @@ def random_shear(x, intensity, row_axis=1, col_axis=2, channel_axis=0,
(one of `{'constant', 'nearest', 'reflect', 'wrap'}`).
cval: Value used for points outside the boundaries
of the input if `mode='constant'`.
interpolation_order int: order of spline interpolation.
interpolation_order: int, order of spline interpolation.
see `ndimage.interpolation.affine_transform`
# Returns
Sheared Numpy image tensor.
"""
Expand All @@ -130,7 +133,7 @@ def random_zoom(x, zoom_range, row_axis=1, col_axis=2, channel_axis=0,
(one of `{'constant', 'nearest', 'reflect', 'wrap'}`).
cval: Value used for points outside the boundaries
of the input if `mode='constant'`.
interpolation_order int: order of spline interpolation.
interpolation_order: int, order of spline interpolation.
see `ndimage.interpolation.affine_transform`
# Returns
Expand Down Expand Up @@ -269,7 +272,7 @@ def apply_affine_transform(x, theta=0, tx=0, ty=0, shear=0, zx=1, zy=1,
(one of `{'constant', 'nearest', 'reflect', 'wrap'}`).
cval: Value used for points outside the boundaries
of the input if `mode='constant'`.
order int: order of interpolation
order: int, order of interpolation
# Returns
The transformed version of the input.
Expand Down
1 change: 1 addition & 0 deletions keras_preprocessing/image/dataframe_iterator.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class DataFrameIterator(BatchFromFilesMixin, Iterator):
If PIL version 1.1.3 or newer is installed, "lanczos" is also
supported. If PIL version 3.4.0 or newer is installed, "box" and
"hamming" are also supported. By default, "nearest" is used.
dtype: Dtype to use for the generated arrays.
validate_filenames: Boolean, whether to validate image filenames in
`x_col`. If `True`, invalid images will be ignored. Disabling this option
can lead to speed-up in the instantiation of this class. Default: `True`.
Expand Down
1 change: 1 addition & 0 deletions keras_preprocessing/image/directory_iterator.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class DirectoryIterator(BatchFromFilesMixin, Iterator):
images (if `save_to_dir` is set).
save_format: Format to use for saving sample images
(if `save_to_dir` is set).
follow_links: boolean,follow symbolic links to subdirectories
subset: Subset of data (`"training"` or `"validation"`) if
validation_split is set in ImageDataGenerator.
interpolation: Interpolation method used to resample the image if the
Expand Down
4 changes: 3 additions & 1 deletion keras_preprocessing/image/image_data_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class ImageDataGenerator(object):
featurewise_std_normalization: Boolean.
Divide inputs by std of the dataset, feature-wise.
samplewise_std_normalization: Boolean. Divide each input by its std.
zca_epsilon: epsilon for ZCA whitening. Default is 1e-6.
zca_whitening: Boolean. Apply ZCA whitening.
zca_epsilon: epsilon for ZCA whitening. Default is 1e-6.
rotation_range: Int. Degree range for random rotations.
width_shift_range: Float, 1-D array-like or int
- float: fraction of total width, if < 1, or pixels if >= 1.
Expand Down Expand Up @@ -100,6 +100,8 @@ class ImageDataGenerator(object):
If you never set it, then it will be "channels_last".
validation_split: Float. Fraction of images reserved for validation
(strictly between 0 and 1).
interpolation_order: int, order to use for
the spline interpolation. Higher is slower.
dtype: Dtype to use for the generated arrays.
# Examples
Expand Down
8 changes: 5 additions & 3 deletions keras_preprocessing/image/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@


def validate_filename(filename, white_list_formats):
"""Check if a filename refers to a valid file
"""Check if a filename refers to a valid file.
# Arguments
filename: String, absolute path to a file
Expand Down Expand Up @@ -81,6 +81,7 @@ def load_img(path, grayscale=False, color_mode='rgb', target_size=None,
# Arguments
path: Path to image file.
grayscale: DEPRECATED use `color_mode="grayscale"`.
color_mode: One of "grayscale", "rgb", "rgba". Default: "rgb".
The desired image format.
target_size: Either `None` (default to original size)
Expand Down Expand Up @@ -139,6 +140,7 @@ def list_pictures(directory, ext=('jpg', 'jpeg', 'bmp', 'png', 'ppm', 'tif',
# Arguments
directory: string, absolute path to the directory
ext: tuple of strings or single string, extensions of the pictures
# Returns
a list of paths
"""
Expand All @@ -156,7 +158,7 @@ def _iter_valid_files(directory, white_list_formats, follow_links):
containing files to be counted
white_list_formats: Set of strings containing allowed extensions for
the files to be counted.
follow_links: Boolean.
follow_links: Boolean, follow symbolic links to subdirectories.
# Yields
Tuple of (root, filename) with extension in `white_list_formats`.
Expand Down Expand Up @@ -189,7 +191,7 @@ def _list_valid_filenames_in_directory(directory, white_list_formats, split,
E.g.: `segment=(0.6, 1.0)` would only account for last 40 percent
of images in each directory.
class_indices: dictionary mapping a class name to its index.
follow_links: boolean.
follow_links: boolean, follow symbolic links to subdirectories.
# Returns
classes: a list of class indices
Expand Down
174 changes: 174 additions & 0 deletions tests/test_documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import importlib
import inspect
import re
import sys
from itertools import compress

import pytest

modules = ['keras_preprocessing',
'keras_preprocessing.image',
'keras_preprocessing.sequence',
'keras_preprocessing.text']

# Tokenizer is being refactored PR #106
accepted_name = ['set_keras_submodules', 'get_keras_submodule', 'Tokenizer']
accepted_module = []

# Functions or classes with less than 'MIN_CODE_SIZE' lines can be ignored
MIN_CODE_SIZE = 10


def handle_class_init(name, member):
init_args = [
arg for arg in list(inspect.signature(member.__init__).parameters.keys())
if arg not in ['self', 'args', 'kwargs']
]
assert_args_presence(init_args, member.__doc__, member, name)


def handle_class(name, member):
if is_accepted(name, member):
return

if member.__doc__ is None and not member_too_small(member):
raise ValueError("{} class doesn't have any documentation".format(name),
member.__module__, inspect.getmodule(member).__file__)

handle_class_init(name, member)

for n, met in inspect.getmembers(member):
if inspect.ismethod(met):
handle_method(n, met)


def handle_function(name, member):
if is_accepted(name, member) or member_too_small(member):
# We don't need to check this one.
return
doc = member.__doc__
if doc is None:
raise ValueError("{} function doesn't have any documentation".format(name),
member.__module__, inspect.getmodule(member).__file__)

args = list(inspect.signature(member).parameters.keys())
assert_args_presence(args, doc, member, name)
assert_function_style(name, member, doc, args)
assert_doc_style(name, member, doc)


def assert_doc_style(name, member, doc):
lines = doc.split("\n")
first_line = lines[0]
if len(first_line.strip()) == 0:
raise ValueError(
"{} the documentation should be on the first line.".format(name),
member.__module__)
first_blank = [i for i, line in enumerate(lines) if not line.strip()]
if len(first_blank) > 0:
if lines[first_blank[0] - 1].strip()[-1] != '.':
raise ValueError("{} first line should end with a '.'".format(name),
member.__module__)


def assert_function_style(name, member, doc, args):
code = inspect.getsource(member)
has_return = re.findall(r"\s*return \S+", code, re.MULTILINE)
if has_return and "# Returns" not in doc:
innerfunction = [inspect.getsource(x) for x in member.__code__.co_consts if
inspect.iscode(x)]
return_in_sub = [ret for code_inner in innerfunction for ret in
re.findall(r"\s*return \S+", code_inner, re.MULTILINE)]
if len(return_in_sub) < len(has_return):
raise ValueError("{} needs a '# Returns' section".format(name),
member.__module__)

has_raise = re.findall(r"^\s*raise \S+", code, re.MULTILINE)
if has_raise and "# Raises" not in doc:
innerfunction = [inspect.getsource(x) for x in member.__code__.co_consts if
inspect.iscode(x)]
raise_in_sub = [ret for code_inner in innerfunction for ret in
re.findall(r"\s*raise \S+", code_inner, re.MULTILINE)]
if len(raise_in_sub) < len(has_raise):
raise ValueError("{} needs a '# Raises' section".format(name),
member.__module__)

if len(args) > 0 and "# Arguments" not in doc:
raise ValueError("{} needs a '# Arguments' section".format(name),
member.__module__)

assert_blank_before(name, member, doc, ['# Arguments', '# Raises', '# Returns'])


def assert_blank_before(name, member, doc, keywords):
doc_lines = [x.strip() for x in doc.split('\n')]
for keyword in keywords:
if keyword in doc_lines:
index = doc_lines.index(keyword)
if doc_lines[index - 1] != '':
raise ValueError(
"{} '{}' should have a blank line above.".format(name, keyword),
member.__module__)


def is_accepted(name, member):
if 'keras_preprocessing' not in str(member.__module__):
return True
return name in accepted_name or member.__module__ in accepted_module


def member_too_small(member):
code = inspect.getsource(member).split('\n')
return len(code) < MIN_CODE_SIZE


def assert_args_presence(args, doc, member, name):
args_not_in_doc = [arg not in doc for arg in args]
if any(args_not_in_doc):
raise ValueError(
"{} {} arguments are not present in documentation ".format(name, list(
compress(args, args_not_in_doc))), member.__module__, member)
words = doc.replace('*', '').split()
# Check arguments styling
styles = [arg + ":" not in words for arg in args]
if any(styles):
raise ValueError(
"{} {} are not style properly 'argument': documentation".format(
name,
list(compress(args, styles))),
member.__module__)

# Check arguments order
indexes = [words.index(arg + ":") for arg in args]
if indexes != sorted(indexes):
raise ValueError(
"{} arguments order is different from the documentation".format(name),
member.__module__, indexes)


def handle_method(name, member):
if name in accepted_name or member.__module__ in accepted_module:
return
handle_function(name, member)


def handle_module(mod):
for name, mem in inspect.getmembers(mod):
if inspect.isclass(mem):
handle_class(name, mem)
elif inspect.isfunction(mem):
handle_function(name, mem)
elif 'keras_preprocessing' in name and inspect.ismodule(mem):
# Only test keras_preprocessing' modules
handle_module(mem)


@pytest.mark.skipif(sys.version_info < (3, 3), reason="requires python3.3")
def test_doc():
for module in modules:
mod = importlib.import_module(module)
handle_module(mod)


if __name__ == '__main__':
pytest.main([__file__])

0 comments on commit 0494094

Please sign in to comment.