Skip to content

Commit

Permalink
Merge pull request #29 from seddonym/spring-clean
Browse files Browse the repository at this point in the history
Spring clean
  • Loading branch information
seddonym committed Mar 25, 2019
2 parents 4630694 + 37885ff commit eaebd5e
Show file tree
Hide file tree
Showing 22 changed files with 169 additions and 54 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Create an ``.importlinter`` file in the root of your project. For example:
.. code-block:: ini
[importlinter]
root_package_name = myproject
root_package = myproject
[importlinter:contract:1]
name=Foo and bar are decoupled
Expand Down
10 changes: 4 additions & 6 deletions docs/custom_contract_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ this are:
Step one: implementing a Contract class
---------------------------------------

You define a custom contract type by subclassing ``importlinter.domain.contracts.Contract`` and implementing the
You define a custom contract type by subclassing ``importlinter.Contract`` and implementing the
following methods:

- ``check(graph)``:
Expand All @@ -23,15 +23,15 @@ following methods:
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,
- An ``importlinter.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.
``importlinter.output`` module.

Arguments:
- ``check``: the ``ContractCheck`` instance returned by the ``check`` method above.
Expand All @@ -45,9 +45,7 @@ see ``importlinter.contracts.layers``.

.. code-block:: python
from importlinter.domain.contract import Contract, ContractCheck
from importlinter.domain import fields
from importlinter.application import output
from importlinter import Contract, ContractCheck, fields, output
class ForbiddenImportContract(Contract):
Expand Down
4 changes: 2 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ As a minimum, your file should contain the following top-level configuration:
.. code-block:: ini
[importlinter]
root_package_name = mypackage
root_package = mypackage
**Options:**

- ``root_package_name``:
- ``root_package``:
The name of the top-level Python package to validate. This package must be importable: usually this
means it is has been installed using pip, or it's in the current directory. (Required.)

Expand Down
20 changes: 0 additions & 20 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,3 @@ exclude = */migrations/*, tests/assets/*
[mypy-pytest]
ignore_missing_imports = True

[import-linter]
root_package_name = grimp

[import-linter:contract:1]
name=My contract from INI file
class=importlinter.contracts.independence.IndependenceContract
modules=
grimp.main
grimp.adaptors

[import-linter:contract:2]
name=Layer contract from INI file
class=importlinter.contracts.layers.LayersContract
containers=
grimp
layers=
adaptors
main
application
domain
4 changes: 4 additions & 0 deletions src/importlinter/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
__version__ = '1.0a1'

from .domain.contract import Contract, ContractCheck # noqa
from .domain import fields # noqa
from .application import output # noqa
3 changes: 3 additions & 0 deletions src/importlinter/adapters/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@


class GraphBuilder(ports.GraphBuilder):
"""
GraphBuilder that just uses Grimp's standard build_graph function.
"""
def build(self, root_package_name: str) -> ImportGraph:
return grimp.build_graph(root_package_name)
3 changes: 3 additions & 0 deletions src/importlinter/adapters/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@


class FileSystem(ports.FileSystem):
"""
File system adapter that delegates to built in file system functions.
"""
def join(self, *components: str) -> str:
return os.path.join(*components)

Expand Down
13 changes: 8 additions & 5 deletions src/importlinter/adapters/printing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@


class ClickPrinter(Printer):
"""
Console printer that uses Click's formatting helpers.
"""
def print(
self,
text: str = '',
bold: bool = False,
color: Optional[str] = None,
newline: bool = True
self,
text: str = '',
bold: bool = False,
color: Optional[str] = None,
newline: bool = True
) -> None:
click.secho(text, bold=bold, fg=color, nl=newline)
3 changes: 3 additions & 0 deletions src/importlinter/adapters/user_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@


class IniFileUserOptionReader(ports.UserOptionReader):
"""
Reader that looks for and parses the contents of INI files.
"""
potential_config_filenames = ('setup.cfg', '.importlinter')
section_name = 'importlinter'

Expand Down
29 changes: 26 additions & 3 deletions src/importlinter/application/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,32 @@


class Output:
"""
A class for writing output to the console.
This should always be used instead of the built in print function, as it uses the Printer
port. This makes it easier for tests to swap in a different Printer so we can more easily
assert what would be written to the console.
"""

def print(
self,
text: str = '',
bold: bool = False,
color: Optional[str] = None,
newline: bool = True
):
) -> None:
"""
Print a line.
Args:
text (str): The text to print.
bold (bool, optional): Whether to style the text in bold. (Default False.)
color (str, optional): The color of text to use. One of the values of the
COLORS dictionary.
newline (bool, optional): Whether to include a new line after the text.
(Default True.)
"""
self.printer.print(text, bold, color, newline)

def indent_cursor(self):
Expand All @@ -41,6 +60,9 @@ def indent_cursor(self):
self.printer.print(' ' * INDENT_SIZE, newline=False)

def new_line(self):
"""
Print a blank line.
"""
self.printer.print()

def print_heading(
Expand All @@ -53,8 +75,9 @@ def print_heading(
Prints the supplied text to the console, formatted as a heading.
Args:
text (str): the text to format as a heading.
level (int): the level of heading to display (one of the keys of HEADING_MAP).
text (str): The text to format as a heading.
level (int): The level of heading to display (one of the keys
of HEADING_MAP).
style (str, optional): ERROR or SUCCESS style to apply (default None).
Usage:
Expand Down
6 changes: 6 additions & 0 deletions src/importlinter/application/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
# ----------------

def render_report(report: Report) -> None:
"""
Output the supplied report to the console.
"""
if report.could_not_run:
_render_could_not_run(report)
return
Expand All @@ -32,6 +35,9 @@ def render_report(report: Report) -> None:


def render_exception(exception: Exception) -> None:
"""
Render any exception to the console.
"""
output.print_error(str(exception))


Expand Down
9 changes: 8 additions & 1 deletion src/importlinter/application/use_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from . rendering import render_report, render_exception


# Public functions
# ----------------

SUCCESS = True
FAILURE = False

Expand Down Expand Up @@ -50,14 +53,18 @@ def create_report(user_options: UserOptions) -> Report:
such as a module that could not be imported.
"""
graph = _build_graph(
root_package_name=user_options.session_options['root_package_name'],
root_package_name=user_options.session_options['root_package'],
)
return _build_report(
graph=graph,
user_options=user_options,
)


# Private functions
# -----------------


def _read_user_options(config_filename: Optional[str] = None) -> UserOptions:
for reader in settings.USER_OPTION_READERS:
options = reader.read_options(config_filename=config_filename)
Expand Down
4 changes: 4 additions & 0 deletions src/importlinter/application/user_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class InvalidUserOptions(Exception):
class UserOptions:
"""
Configuration supplied by the end user.
Arguments:
- session_options: General options relating to the running of the linter.
- contracts_options: List of the options that will be used to build the contracts.
"""
def __init__(
self,
Expand Down
15 changes: 15 additions & 0 deletions src/importlinter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,25 @@
@click.command()
@click.option('--config', default=None, help='The config file to use.')
def lint_imports_command(config: Optional[str]) -> int:
"""
The entry point for the CLI command.
"""
return lint_imports(config_filename=config)


def lint_imports(config_filename: Optional[str] = None) -> int:
"""
Check that a project adheres to a set of contracts.
This is the main function that runs the linter.
Args:
config_filename: The configuration file to use. If not supplied, Import Linter will look
for setup.cfg or .importlinter in the current directory.
Returns:
EXIT_STATUS_SUCCESS or EXIT_STATUS_ERROR.
"""
# Add current directory to the path, as this doesn't happen automatically.
sys.path.insert(0, os.getcwd())

Expand Down
13 changes: 13 additions & 0 deletions src/importlinter/contracts/independence.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@


class IndependenceContract(Contract):
"""
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.
Configuration options:
- modules: A list of Modules that should be independent from each other.
- ignore_imports: A list of DirectImports. 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.)
"""
type_name = 'independence'

modules = fields.ListField(subfield=fields.ModuleField())
Expand Down
21 changes: 19 additions & 2 deletions src/importlinter/contracts/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ def __init__(self, name: str, is_optional: bool = False) -> None:


class LayerField(fields.Field):
return_type = Layer # type: ignore

def parse(self, raw_data: Union[str, List]) -> Layer:
raw_string = fields.StringField().parse(raw_data)
if raw_string.startswith('(') and raw_string.endswith(')'):
Expand All @@ -29,6 +27,25 @@ def parse(self, raw_data: Union[str, List]) -> Layer:


class LayersContract(Contract):
"""
Defines a 'layered architecture' where there is a unidirectional dependency flow.
Specifically, higher layers may depend on lower layers, but not the other way around.
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.
Configuration options:
- layers: An ordered list of layers. Each layer is the name of a module relative
to its parent package. The order is from higher to lower level layers.
- containers: A list of the parent Modules of the layers.
- ignore_imports: A list of DirectImports. 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.)
"""
type_name = 'layers'

containers = fields.ListField(subfield=fields.StringField())
Expand Down

0 comments on commit eaebd5e

Please sign in to comment.