Skip to content

Commit

Permalink
Merge pull request #28 from seddonym/docs
Browse files Browse the repository at this point in the history
Docs
  • Loading branch information
seddonym committed Mar 25, 2019
2 parents 0c8940f + bff67d8 commit 4630694
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 8 deletions.
93 changes: 91 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,97 @@ Import Linter
:target: https://travis-ci.org/seddonym/import-linter


Import Linter allows you to define and enforce rules for the internal and external imports within your Python project.
Import Linter allows you to define and enforce rules for the internal imports within your Python project.

* Free software: BSD license
* Documentation: https://import-linter.readthedocs.io.

**Warning:** This software is currently being developed; it is not ready to use.
**Warning:** This software is currently in alpha. This means there are likely to be changes that break backward
compatibility. However, due to it being a development tool (rather than something that needs to be installed
on a production system), it may be suitable for inclusion in your testing pipeline. It also means we actively
encourage people to try it out and `submit bug reports`_.

.. _submit bug reports: https://import-linter.readthedocs.io/en/stable/contributing.html#report-bugs

Overview
--------

Import Linter is a command line tool to check that you are following a self-imposed
architecture within your Python project. It does this by analysing the internal
imports between all the modules in your code base, and compares this
against a set of rules that you provide in a configuration file.

The configuration file contains one or more 'contracts'. Each contract has a specific
type, which determines the sort of rules it will apply. For example, the ``independence``
contract type checks that there are no imports, in either direction, between a set
of subpackages.

Import Linter is particularly useful if you are working on a complex codebase within a team,
when you want to enforce a particular architectural style. In this case you can add
Import Linter to your deployment pipeline, so that any code that does not follow
the architecture will fail tests.

If there isn't a built in contract type that fits your desired architecture, you can define
a custom one.

Quick start
-----------

Install Import Linter::

pip install import-linter

Decide on the dependency flows you wish to check. In this example, we have
decided to make sure that there are no dependencies between ``myproject.foo``
and ``myproject.bar``, so we will use the ``independence`` contract type.

Create an ``.importlinter`` file in the root of your project. For example:

.. code-block:: ini
[importlinter]
root_package_name = myproject
[importlinter:contract:1]
name=Foo and bar are decoupled
type=independence
modules=
myproject.foo
myproject.bar
Now, from your project root, run::

lint-imports

If your code violates the contract, you will see an error message something like this:

.. code-block:: none
=============
Import Linter
=============
---------
Contracts
---------
Analyzed 23 files, 44 dependencies.
-----------------------------------
Foo and bar are decoupled BROKEN
Contracts: 1 broken.
----------------
Broken contracts
----------------
Foo and bar are decoupled
-------------------------
myproject.foo is not allowed to import myproject.bar:
- myproject.foo.blue -> myproject.utils.red (l.16)
myproject.utils.red -> myproject.utils.green (l.1)
myproject.utils.green -> myproject.bar.yellow (l.3)
108 changes: 108 additions & 0 deletions docs/contract_types.rst
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`.
114 changes: 114 additions & 0 deletions docs/custom_contract_types.rst
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
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Contents
readme
installation
usage
contract_types
custom_contract_types
contributing
authors
changelog
Expand Down
2 changes: 1 addition & 1 deletion docs/readme.rst
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`.

0 comments on commit 4630694

Please sign in to comment.