Advanced usage

Write you own validators

How validators work

Of course, the standard suit of validators can be modified in a way that best suits your needs.

The are two kinds of validators: error validators and warning validators. The difference between them is that warning validators don’t halt the validation process, while the error validators do. Error validators are grouped according to their purpose, like in this code

error_validator_groups = OrderedDict(
    [
        (
            'commits',
            [validators.has_more_commits_than_origin],
        ),
        (
            'readme',
            [validators.has_readme_file],
        ),
        ...
    ]
)

Here, for example, you have the group commits that consists of the only has_more_commits_than_origin validator.

In each group, every validator is executed. If some of the validators in the group fail, the validate method returns the error list without proceeding to the next group. If all the validators in the error group succeed, the warning validators for this group are executed. They are stored in warning_validator_groups:

warning_validator_groups = {
    'commits': [
        validators.has_no_commit_messages_from_blacklist,
    ],
    'syntax': [
        validators.has_indents_of_spaces,
        validators.has_no_variables_that_shadow_default_names,
    ]
}

The commits warning validator group is executed only if the commits error validator group passes successfully.

Warning validators just add some more errors in case the validation failed. They are not executed if none of the error validators failed.

Add a simple validator

A simple validator is a validator that only takes the repository (more precisely, LocalRepositoryInfo object) to validate. It returns None is case of success and a tuple of an error slug and an error message in case of a problem. Here’s an example of existing validator:

def has_no_syntax_errors(solution_repo, *args, **kwargs):
    for filename, tree in solution_repo.get_ast_trees(with_filenames=True):
        if tree is None:
            return 'syntax_error', %s' % filename

Note the *args, **kwargs part. The validator actually gets a lot of arguments, but doesn’t care about them.

Now you can add validator to one of the existing validator groups or create your own:

code_validator.error_validator_groups[‘general’].append(has_no_syntax_errors)

Compare against some “original” repo

If you want your validator to compare against some other repository, add the original_repo argument.

def has_more_commits_than_origin(solution_repo, original_repo=None, *args, **kwargs):
    if not original_repo:
        return
    if solution_repo.count_commits() <= original_repo.count_commits():
        return 'no_new_code', None

Notice we made our validator succeed in case there’s no original_repo. We consider it a sensible solution for our case, but you can choose any other behavior.

Parameterize your validator

To add a parameter to your validator, just add it to the arguments.

def has_no_long_files(solution_repo, max_number_of_lines, *args, **kwargs):
    for file_path, file_content, _ in solution_repo.get_ast_trees(with_filenames=True, with_file_content=True):
        number_of_lines = file_content.count('\n')
        if number_of_lines > max_number_of_lines:
            file_name = url_helpers.get_filename_from_path(file_path)
            return 'file_too_long', file_name

and then don’t forget to pass it:

code_validator.validate(repo, max_number_of_lines=200)

Of course, built-in validators have their own defaults in _default_settings property of CodeValidator class.

Conditionally execute a validator

If you want the validator to be executed only for certain types of repositories, add tokenized_validator to it:

from fiasko_bro import tokenized_validator

@tokenized_validator(token='min_max_challenge')
def has_min_max_functions(solution_repo, *args, **kwargs):
    for tree in solution_repo.get_ast_trees():
        names = get_all_names_from_tree(tree)
        if 'min' in names and 'max' in names:
            return
    return 'builtins', 'no min or max is used'

then add the validator to the appropriate group

code_validator.error_validator_groups[‘general’].append(has_min_max_functions)

and when calling validate for certain repo, pass the token:

code_validator.validate(solution_repo=solution_repo, validator_token=’min_max_challenge’)

The validator won’t be executed for any other repository.

Blacklist/whitelists for validators

For every rule there’s an exception. Exceptions are easy to take into account using blacklists or whitelists.

First, add the blacklist and whitelist to the code_validator instance:

code_validator.whitelists['has_no_calls_with_constants'] = ['pow', 'exit']

Then create and add the validator with the same name as the dictionary key:

def has_no_calls_with_constants(solution_repo, whitelists, *args, **kwargs):
    whitelist = whitelists.get('has_no_calls_with_constants', [])
    for filepath, tree in solution_repo.get_ast_trees(with_filenames=True):
        if 'tests' in filepath:  # tests can have constants in asserts
            continue
        calls = [n for n in ast.walk(tree) if isinstance(n, ast.Call)]
        for call in calls:
            if isinstance(ast_helpers.get_closest_definition(call), ast.ClassDef):  # for case of id = db.String(256)
                continue
            attr_to_get_name = 'id' if hasattr(call.func, 'id') else 'attr'
            function_name = getattr(call.func, attr_to_get_name, None)
            if not function_name or function_name in whitelist:
                continue
            for arg in call.args:
                if isinstance(arg, ast.Num):
                    return 'magic_numbers', 'например, %s' % arg.n

Notice in the first line we pull the whitelist from the dictionary and incorporate it in our validation logic.