Example #1
0
    def validate(self,
                 modules: Optional[List[str]] = None) -> ValidationResult:
        """Validate the found modules using the provided reporter.

        Args:
            modules (Optional[List[str]]): Optionally, specify directly the modules rather than discover.

        Returns:
            ValidationResult: Information about whether validation succeeded.
        """
        logging.log('Starting validating')
        result = ValidationResult()

        if modules is None:
            modules = self.discover_modules()
            logging.log(f'Found {len(modules)} modules')

        for module in modules:
            module_result = self.validate_module(module)
            if module_result.result == ResultType.FAILED:
                result.result = ResultType.FAILED
            result.module_results.append(module_result)

        if result.result == ResultType.NOT_RUN:
            result.result = ResultType.OK

        return result
Example #2
0
def validate_class(class_instance: Any, config: Configuration,
                   module_type: ModuleType) -> ClassValidationResult:
    """Validates the class by validating each of its methods.

    Args:
        class_instance (Any): A class to validate.
        config (Configuration): The configuration to use while validating.
        module_type (ModuleType): The module from which the class was extracted.

    Returns:
        ClassValidationResult: The result of validating this class.
    """
    log(f"Validating class: {class_instance}")
    class_result = ClassValidationResult(class_instance.__name__)

    # for name, item in class_instance.__dict__.items():
    for name, item in inspect.getmembers(class_instance):
        if inspect.isfunction(
                item) and item.__module__ == module_type.__name__:
            if name not in class_instance.__dict__ or item != class_instance.__dict__[
                    name]:
                continue
            function_result = validate_function(item, config, module_type)
            if function_result.result == ResultType.FAILED:
                class_result.result = ResultType.FAILED
            class_result.function_results.append(function_result)

    # If result has not been changed at this point, it must be OK
    if class_result.result == ResultType.NOT_RUN:
        class_result.result = ResultType.OK

    return class_result
Example #3
0
    def validate_module(self, module_path: str) -> ModuleValidationResult:
        """Validates the module, given its path.

        Args:
            module_path (str): Path to a module.

        Returns:
            ModuleValidationResult: Result of validating the module.
        """
        logging.log(f'Validating module: {module_path}')
        result = ModuleValidationResult(module_path)

        module_name = os.path.basename(module_path)
        module_spec: Optional[
            ModuleSpec] = importlib.util.spec_from_file_location(
                module_name, module_path)

        if not os.path.exists(
                module_path) or module_spec is None or not isinstance(
                    module_spec.loader, Loader):
            result.result = ResultType.NOT_RUN
            result.fail_reason = f"Failed to load file from location: {module_path}"
            return result

        try:
            module_type = importlib.util.module_from_spec(module_spec)
            module_spec.loader.exec_module(module_type)
        except ModuleNotFoundError as e:
            result.result = ResultType.FAILED
            result.fail_reason = f"Failed to load module dependant module: {str(e)}"
            return result
        except Exception as e:
            result.result = ResultType.FAILED
            result.fail_reason = f"Failed to load module (possibly due to syntax errors): {module_path}"
            return result

        # Validate top-level functions in module
        fns = self.get_global_functions(module_type)
        for fn in fns:
            function_result = validate_function(fn, self.config, module_type)
            if function_result.result == ResultType.FAILED:
                result.result = ResultType.FAILED
            result.function_results.append(function_result)

        # Validate top-level classes in module
        classes = self.get_classes(module_type)
        for cl in classes:
            class_result = validate_class(cl, self.config, module_type)
            if class_result.result == ResultType.FAILED:
                result.result = ResultType.FAILED
            result.class_results.append(class_result)

        return result
Example #4
0
    def get_default_configuration(root_dir: Optional[str] = None) -> 'Configuration':
        """Returns a configuration with default values.

        Args:
            root_dir (Optional[str]): Directory to use as root.

        Returns:
            'Configuration': A default configuration.
        """
        log("Using default configuration")
        config = Configuration()
        if root_dir:
            config.working_directory = root_dir
        return config
Example #5
0
    def get_configuration_from_path(config_path: str) -> 'Configuration':
        """Returns a configuration, loaded from the pydoctest.json provided.

        Args:
            config_path (str): The path to a config file.

        Returns:
            'Configuration': A configuration loaded with values from provided path.
        """
        config_path_absolute = os.path.abspath(config_path)
        log(f"Using configuration from path: {config_path_absolute}")

        with open(config_path_absolute, 'r') as f:
            config_dict = json.load(f)
            config = Configuration.from_dict(config_dict)
            config.working_directory = os.path.dirname(config_path_absolute)
            return config
Example #6
0
def validate_function(fn: FunctionType, config: Configuration,
                      module_type: ModuleType) -> FunctionValidationResult:
    """Validates the docstring of a function against its signature.

    Args:
        fn (FunctionType): The function to validate.
        config (Configuration): The configuration to use while validating.
        module_type (ModuleType): The module from which the function was extracted.

    Returns:
        FunctionValidationResult: The result of validating this function.
    """
    log(f"Validating function: {fn}")
    result = FunctionValidationResult(fn, module_type)

    doc = inspect.getdoc(fn)
    if not doc:
        if config.fail_on_missing_docstring:
            result.result = ResultType.FAILED
            result.fail_reason = f"Function does not have a docstring"
            _, line_number = inspect.getsourcelines(fn)
            result.range = Range(line_number, line_number, 0, 0)
        else:
            result.result = ResultType.NO_DOC
        return result

    parser = config.get_parser()

    summary = parser.get_summary(doc, module_type)
    if not summary and config.fail_on_missing_summary:
        result.result = ResultType.FAILED
        result.fail_reason = f"Function does not have a summary"
        result.range = __get_docstring_range(fn, module_type, doc)
        return result

    sig = inspect.signature(fn)
    sig_parameters = [
        Parameter(name, proxy.annotation)
        for name, proxy in sig.parameters.items() if name != "self"
    ]
    sig_return_type = type(
        None) if sig.return_annotation is None else sig.return_annotation

    try:
        doc_parameters = parser.get_parameters(doc, module_type)
        doc_return_type = parser.get_return_type(doc, module_type)
    except ParseException as e:
        result.result = ResultType.FAILED
        result.fail_reason = f"Unable to parse docstring: {str(e)}"
        result.range = __get_docstring_range(fn, module_type, doc)
        return result

    # Validate return type
    if sig_return_type != doc_return_type:
        result.result = ResultType.FAILED
        result.fail_reason = f"Return type differ. Expected (from signature) {sig_return_type}, but got (in docs) {doc_return_type}."
        result.range = __get_docstring_range(fn, module_type, doc)
        return result

    # Validate equal number of parameters
    if len(sig_parameters) != len(doc_parameters):
        result.result = ResultType.FAILED
        result.fail_reason = f"Number of arguments differ. Expected (from signature) {len(sig_parameters)} arguments, but found (in docs) {len(doc_parameters)}."
        result.range = __get_docstring_range(fn, module_type, doc)
        return result

    # Validate name and type of function parameters
    for sigparam, docparam in zip(sig_parameters, doc_parameters):
        if sigparam.name != docparam.name:
            result.result = ResultType.FAILED
            result.fail_reason = f"Argument name differ. Expected (from signature) '{sigparam.name}', but got (in docs) '{docparam.name}'"
            result.range = __get_docstring_range(fn, module_type, doc)
            return result

        # NOTE: Optional[str] == Union[str, None] # True
        if sigparam.type != docparam.type:
            result.result = ResultType.FAILED
            result.fail_reason = f"Argument type differ. Argument '{sigparam.name}' was expected (from signature) to have type '{sigparam.type}', but has (in docs) type '{docparam.type}'"
            result.range = __get_docstring_range(fn, module_type, doc)
            return result

    # Validate exceptions raised
    if config.fail_on_raises_section:
        try:
            sig_exceptions = get_exceptions_raised(fn, module_type)
            doc_exceptions = parser.get_exceptions_raised(doc)

            if len(sig_exceptions) != len(doc_exceptions):
                result.result = ResultType.FAILED
                result.fail_reason = f"Number of listed raised exceptions does not match actual. Doc: {doc_exceptions}, expected: {sig_exceptions}"
                result.range = __get_docstring_range(fn, module_type, doc)
                return result

            intersection = set(sig_exceptions) - set(doc_exceptions)
            if len(intersection) > 0:
                result.result = ResultType.FAILED
                result.fail_reason = f"Listed raised exceptions does not match actual. Docstring: {doc_exceptions}, expected: {sig_exceptions}"
                result.range = __get_docstring_range(fn, module_type, doc)
                return result
        except ParseException as e:
            result.result = ResultType.FAILED
            result.fail_reason = f"Unable to parse docstring: {str(e)}"
            result.range = __get_docstring_range(fn, module_type, doc)
            return result

    result.result = ResultType.OK
    return result