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
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
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
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
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
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