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
Contract
class 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.
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 a
ContractCheck
describing whether the contract was adhered to.- Arguments:
graph
: a GrimpImportGraph
of 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 inverbose mode <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.ContractCheck
instance. 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_contract
method that you also define in this class.
- An
render_broken_contract(check: ContractCheck) -> None
:Renders the results of a broken contract check. For output, this should use the
importlinter.output
module.- Arguments:
check
: theContractCheck
instance returned by thecheck
method above.
Contract fields
A contract will usually need some further configuration. This can be done using fields. For an example, see importlinter.contracts.layers
.
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}')
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_name = mypackage
contract_types =
forbidden_import: somepackage.contracts.ForbiddenImportContract
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