-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #28 from seddonym/docs
Docs
- Loading branch information
Showing
10 changed files
with
396 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
============== | ||
Contract types | ||
============== | ||
|
||
Independence | ||
------------ | ||
|
||
*Type name:* ``independence`` | ||
|
||
Independence contracts check that a set of modules do not depend on each other. | ||
|
||
They do this by checking that there are no imports in any direction between the modules, even indirectly. | ||
|
||
**Example:** | ||
|
||
.. code-block:: ini | ||
[importlinter:contract:1] | ||
name = My independence contract | ||
type = independence | ||
modules = | ||
mypackage.foo | ||
mypackage.bar | ||
mypackage.baz | ||
ignore_imports = | ||
mypackage.bar.green -> mypackage.utils | ||
mypackage.baz.blue -> mypackage.foo.purple | ||
**Configuration options** | ||
|
||
- ``modules``: A list of modules/subpackages that should be independent from each other. | ||
- ``ignore_imports``: | ||
A list of imports, each in the form ``mypackage.foo.importer -> mypackage.bar.imported``. These imports | ||
will be ignored: if the import would cause a contract to be broken, adding it to the list will cause the | ||
contract be kept instead. (Optional.) | ||
|
||
Layers | ||
------ | ||
|
||
*Type name:* ``layers`` | ||
|
||
Layers contracts enforce a 'layered architecture', where higher layers may depend on lower layers, but not the other | ||
way around. | ||
|
||
They do this by checking, for an ordered list of modules, that none higher up the list imports anything from a module | ||
lower down the list, even indirectly. To allow for a repeated pattern of layers across a project, you also define | ||
a set of 'containers', which are treated as the parent package of the layers. | ||
|
||
Layers are required by default: if a layer is listed in the contract, the contract will be broken if the layer | ||
doesn't exist. You can make a layer optional by wrapping it in parentheses. | ||
|
||
**Examples** | ||
|
||
.. code-block:: ini | ||
[importlinter:contract:1] | ||
name = My three-tier layers contract | ||
type = layers | ||
layers= | ||
high | ||
medium | ||
low | ||
containers= | ||
mypackage | ||
This contract will not allow imports from lower layers to higher layers. For example, it will not allow | ||
``mypackage.low`` to import ``mypackage.high``, even indirectly. | ||
|
||
.. code-block:: ini | ||
[importlinter:contract:1] | ||
name = My multiple package layers contract | ||
type = layers | ||
layers= | ||
high | ||
(medium) | ||
low | ||
containers= | ||
mypackage.foo | ||
mypackage.bar | ||
mypackage.baz | ||
In this example, each container has its own layered architecture. For example, it will not allow ``mypackage.foo.low`` | ||
to import ``mypackage.foo.high``. However, it will allow ``mypackage.foo.low`` to import ``mypackage.bar.high``, | ||
as they are in different containers: | ||
|
||
Notice that ``medium`` is an optional layer. This means that if it is missing from any of the containers, Import Linter | ||
won't complain. | ||
|
||
**Configuration options** | ||
|
||
- ``layers``: | ||
An ordered list with the name of each layer module, *relative to its parent package*. The order is from higher | ||
to lower level layers. | ||
- ``containers``: | ||
List of the parent modules of the layers, as *absolute names* that you could import, such as | ||
``mypackage.foo``. If you only have one set of layers, there will only be one container. | ||
- ``ignore_imports``: | ||
A list of imports, each in the form ``mypackage.foo.importer -> mypackage.bar.imported``. These imports | ||
will be ignored: if the import would cause a contract to be broken, adding it to the list will cause the | ||
contract be kept instead. (Optional.) | ||
|
||
|
||
Custom contract types | ||
--------------------- | ||
|
||
If none of the built in contract types meets your needs, you can define a custom contract type: see | ||
:doc:`custom_contract_types`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
===================== | ||
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: | ||
|
||
1. Somewhere in your Python path, create a module that implements a ``Contract`` class for your supplied type. | ||
2. Register the contract type in your configuration file. | ||
3. 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.domain.contracts.Contract`` and implementing the | ||
following methods: | ||
|
||
- ``check(graph)``: | ||
Given an import graph of your project, return a ``ContractCheck`` describing whether the contract was adhered to. | ||
|
||
Arguments: | ||
- ``graph``: a Grimp ``ImportGraph`` of your project, which can be used to inspect / analyse any dependencies. | ||
For full details of how to use this, see the `Grimp documentation`_. | ||
|
||
Returns: | ||
- An ``importlinter.domain.contracts.ContractCheck`` instance. This is a simple dataclass with two attributes, | ||
``kept`` (a boolean indicating if the contract was kept) and ``metadata`` (a dictionary of data about the | ||
check). The metadata can contain anything you want, as it is only used in the ``render_broken_contract`` | ||
method that you also define in this class. | ||
|
||
- ``render_broken_contract(check)``: | ||
|
||
Renders the results of a broken contract check. For output, this should use the | ||
``importlinter.application.output`` module. | ||
|
||
Arguments: | ||
- ``check``: the ``ContractCheck`` instance returned by the ``check`` 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** | ||
|
||
.. code-block:: python | ||
from importlinter.domain.contract import Contract, ContractCheck | ||
from importlinter.domain import fields | ||
from importlinter.application import output | ||
class ForbiddenImportContract(Contract): | ||
""" | ||
Contract that defines a single forbidden import between | ||
two modules. | ||
""" | ||
importer = fields.StringField() | ||
imported = fields.StringField() | ||
def check(self, graph): | ||
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: | ||
|
||
.. code-block:: ini | ||
[importlinter] | ||
root_package_name = 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: | ||
|
||
.. code-block:: ini | ||
[importlinter:contract:1] | ||
name = My custom contract | ||
type = forbidden_import | ||
importer = mypackage.foo | ||
imported = mypackage.bar | ||
.. _Grimp documentation: https://grimp.readthedocs.io |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
.. include:: ../README.rst | ||
|
||
For the full list of methods, see :doc:`usage`. | ||
For more details, see :doc:`usage`. |
Oops, something went wrong.