Custom contract types
If none of the built in contract types serve your needs, you can define a custom contract type. The steps to do this are:
- Somewhere in your Python path, create a module that implements a
Contractclass for your supplied type. - Register the contract type in your configuration file.
- Define one or more contracts of your custom type, also in your configuration file.
Step one: implementing a Contract class
You define a custom contract type by subclassing importlinter.Contract and implementing the
following methods:
-
check(graph: ImportGraph, verbose: bool) -> ContractCheck: Given an import graph of your project, return aContractCheckdescribing whether the contract was adhered to.Arguments:
graph: a GrimpImportGraphof your project, which can be used to inspect / analyse any dependencies. For full details of how to use this, see the Grimp documentation.verbose: Whether we're in verbose mode. You can use this flag to determine whether to output text during the check, usingoutput.verbose_print, as in the example below.
Returns:
- An
importlinter.ContractCheckinstance. This is a simple dataclass with two attributes,kept(a boolean indicating if the contract was kept) andmetadata(a dictionary of data about the check). The metadata can contain anything you want, as it is only used in therender_broken_contractmethod that you also define in this class.
-
render_broken_contract(check: ContractCheck) -> None:Renders the results of a broken contract check. For output, this should use the
importlinter.outputmodule.Arguments:
check: theContractCheckinstance returned by thecheckmethod above.
Contract fields
The following field types are available:
StringFieldaccepts a string value.BooleanFieldaccepts a boolean value.IntegerFieldaccepts an integer value.EnumFieldaccepts a value from a predefined set of string options.ModuleFieldaccepts a string value and validates that it refers to a valid Python module.ModuleExpressionFieldacts in a similar way asModuleField, but allows wildcard expressions.ImportExpressionFieldaccepts a string value representing a specific import, module description can include wildcards. (example:mypackage.foo.** -> mypackage.bar)ListFieldaccepts multiple values of the field type specified in the subfield parameter. Duplicate values are allowed, and the values are returned in the order they appear in the configuration.SetFieldaccepts multiple values of the field type specified in the subfield parameter. Duplicate values are not allowed, and they are not ordered.
Fields definition can be found in importlinter.domain.fields.
Fields are to be added in the class definition, the base Contract constructor will parse the value from the config and inject it into the instance with the same name. (this will make type checker complain about field usage)
Example of multiple values fields usage can be found in importlinter.contracts.forbidden
Example custom contract
from importlinter import Contract, ContractCheck, fields, output
class ForbiddenImportContract(Contract):
"""
Contract that defines a single forbidden import between
two modules.
"""
importer = fields.StringField()
imported = fields.StringField()
def check(self, graph, verbose):
output.verbose_print(
verbose,
f"Getting import details from {self.importer} to {self.imported}..."
)
forbidden_import_details = graph.get_import_details(
importer=self.importer,
imported=self.imported,
)
import_exists = bool(forbidden_import_details)
return ContractCheck(
kept=not import_exists,
metadata={
'forbidden_import_details': forbidden_import_details,
}
)
def render_broken_contract(self, check):
output.print_error(
f'{self.importer} is not allowed to import {self.imported}:',
bold=True,
)
output.new_line()
for details in check.metadata['forbidden_import_details']:
line_number = details['line_number']
line_contents = details['line_contents']
output.indent_cursor()
output.print_error(f'{self.importer}:{line_number}: {line_contents}')
Step two: register the contract type
In the [importlinter] section of your configuration file, include a list of contract_types that map type names
onto the Python path of your custom class:
[importlinter]
root_package = mypackage
contract_types=
forbidden_import: somepackage.contracts.ForbiddenImportContract
[tool.importlinter]
root_package = "mypackage"
contract_types = [
"forbidden_import: somepackage.contracts.ForbiddenImportContract"
]
Step three: define your contracts
You may now use the type name defined in the previous step to define a contract:
[importlinter:contract:my-custom-contract]
name = My custom contract
type = forbidden_import
importer = mypackage.foo
imported = mypackage.bar
[[tool.importlinter.contracts]]
name = "My custom contract"
type = "forbidden_import"
importer = "mypackage.foo"
imported = "mypackage.bar"