Skip to content

Commit

Permalink
Allow limiting by contract
Browse files Browse the repository at this point in the history
  • Loading branch information
seddonym committed Jan 27, 2023
1 parent 95de602 commit 58f797f
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ latest

* Switch from optional dependency of ``toml`` to required dependency of ``tomli`` for Python versions < 3.11.
* Use DetailedImport type hinting made available in Grimp 2.2.
* Allow limiting by contract.

1.6.0 (2022-12-7)
-----------------
Expand Down
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autosectionlabel',
]

# Make sure the target is unique
autosectionlabel_prefix_document = True

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

Expand Down
14 changes: 7 additions & 7 deletions docs/contract_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ External packages may also be forbidden.
[importlinter]
root_package = mypackage
[importlinter:contract:1]
[importlinter:contract:my-forbidden-contract]
name = My forbidden contract (internal packages only)
type = forbidden
source_modules =
Expand All @@ -41,7 +41,7 @@ External packages may also be forbidden.
root_package = mypackage
include_external_packages = True
[importlinter:contract:1]
[importlinter:contract:my-forbidden-contract]
name = My forbidden contract (internal and external packages)
type = forbidden
source_modules =
Expand Down Expand Up @@ -80,7 +80,7 @@ They do this by checking that there are no imports in any direction between the

.. code-block:: ini
[importlinter:contract:1]
[importlinter:contract:my-independence-contract]
name = My independence contract
type = independence
modules =
Expand Down Expand Up @@ -127,7 +127,7 @@ exhaustive contracts are only supported for layers that define containers.
[importlinter]
root_package = mypackage
[importlinter:contract:1]
[importlinter:contract:my-layers-contract]
name = My three-tier layers contract
type = layers
layers=
Expand All @@ -146,7 +146,7 @@ This contract will not allow imports from lower layers to higher layers. For exa
medium
low
[importlinter:contract:1]
[importlinter:contract:my-layers-contract]
name = My three-tier layers contract (multiple root packages)
type = layers
layers=
Expand All @@ -161,7 +161,7 @@ In this case, ``high``, ``medium`` and ``low`` all need to be specified as ``roo

.. code-block:: ini
[importlinter:contract:1]
[importlinter:contract:my-layers-contract]
name = My multiple package layers contract
type = layers
layers=
Expand All @@ -184,7 +184,7 @@ This is an example of an 'exhaustive' contract.

.. code-block:: ini
[importlinter:contract:1]
[importlinter:contract:my-layers-contract]
name = My multiple package layers contract
type = layers
layers=
Expand Down
2 changes: 1 addition & 1 deletion docs/custom_contract_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ You may now use the type name defined in the previous step to define a contract:

.. code-block:: ini
[importlinter:contract:1]
[importlinter:contract:my-custom-contract]
name = My custom contract
type = forbidden_import
importer = mypackage.foo
Expand Down
10 changes: 9 additions & 1 deletion docs/toml.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
============
TOML support
------------
============


While all the examples are in INI format, Import Linter also supports TOML.

Expand Down Expand Up @@ -40,3 +42,9 @@ Following, an example with a layered configuration:
"medium",
"low",
]
Contract ids
------------

You can optionally provide an ``id`` key for each contract. This allows
you to make use of the ``--contract`` parameter when :ref:`running the linter<usage:Running the linter>`.
22 changes: 17 additions & 5 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,19 @@ Additionally, you will want to include one or more contract configurations. Thes

.. code-block:: ini
[importlinter:contract:1]
[importlinter:contract:one]
name = Contract One
type = some_contract_type
(additional options)
[importlinter:contract:2]
[importlinter:contract:two]
name = Contract Two
type = another_contract_type
(additional options)
Notice each contract has its own INI section, which begins ``importlinter:contract:`` and ends in an
arbitrary, unique code (in this example, the codes are ``1`` and ``2``). These codes are purely
to adhere to the INI format, which does not allow duplicate section names.
Notice each contract has its own INI section, which begins ``importlinter:contract:`` and ends in a
unique id (in this example, the ids are ``one`` and ``two``). These codes can be used to
to select individual contracts when running the linter (see below).

Every contract will always have the following key/value pairs:

Expand All @@ -97,6 +97,12 @@ Running this will check that your project adheres to the contracts you've define
The configuration file to use. This overrides the default file search strategy.
By default it's assumed that the file is an ini-file unless the file extension is ``toml``.
(Optional.)
- ``--contract``:
Limit the check to the contract with the supplied id. In INI files, a contract's id is
the final part of the section header: for example, the id for a contract with a section
header of ``[importlinter:contract:foo]`` is ``foo``. In TOML files, ids are supplied
explicitly with an ``id`` key. This option may be provided multiple
times to check more than one contract. (Optional.)
- ``--show_timings``:
Display the times taken to build the graph and check each contract. (Optional.)
- ``--verbose``:
Expand All @@ -114,6 +120,12 @@ Running this will check that your project adheres to the contracts you've define
lint-imports --config path/to/alternative-config.ini
**Checking only certain contracts:**

.. code-block:: text
lint-imports --contract some-contract --contract another-contract
**Showing timings:**

.. code-block:: text
Expand Down
4 changes: 3 additions & 1 deletion src/importlinter/adapters/user_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def _build_from_config(self, config: configparser.ConfigParser) -> UserOptions:
contract_options = []
for section_name in config.sections():
if section_name.startswith(f"{self.section_name}:"):
contract_options.append(self._clean_section_config(dict(config[section_name])))
contract_option = {"id": section_name.split(":")[-1]}
contract_option.update(self._clean_section_config(dict(config[section_name])))
contract_options.append(contract_option)
return UserOptions(session_options=session_options, contracts_options=contract_options)

@staticmethod
Expand Down
57 changes: 47 additions & 10 deletions src/importlinter/application/use_cases.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import importlib
from copy import copy, deepcopy
from typing import List, Optional, Tuple, Type
from typing import Any, Dict, List, Optional, Tuple, Type

from ..application import rendering
from ..domain.contract import Contract, InvalidContractOptions, registry
Expand All @@ -20,6 +20,7 @@

def lint_imports(
config_filename: Optional[str] = None,
limit_to_contracts: Tuple[str, ...] = (),
is_debug_mode: bool = False,
show_timings: bool = False,
verbose: bool = False,
Expand All @@ -30,12 +31,13 @@ def lint_imports(
This function attempts to handle and report all exceptions, too.
Args:
config_filename: the filename to use to parse user options.
is_debug_mode: whether debugging should be turned on. In debug mode, exceptions are
not swallowed at the top level, so the stack trace can be seen.
show_timings: whether to show the times taken to build the graph and to check
each contract.
verbose: if True, noisily output progress as it goes along.
config_filename: the filename to use to parse user options.
limit_to_contracts: if supplied, only lint the contracts with the supplied ids.
is_debug_mode: whether debugging should be turned on. In debug mode, exceptions are
not swallowed at the top level, so the stack trace can be seen.
show_timings: whether to show the times taken to build the graph and to check
each contract.
verbose: if True, noisily output progress as it goes along.
Returns:
True if the linting passed, False if it didn't.
Expand All @@ -45,7 +47,7 @@ def lint_imports(
try:
user_options = read_user_options(config_filename=config_filename)
_register_contract_types(user_options)
report = create_report(user_options, show_timings, verbose)
report = create_report(user_options, limit_to_contracts, show_timings, verbose)
except Exception as e:
if is_debug_mode:
raise e
Expand Down Expand Up @@ -86,7 +88,10 @@ def read_user_options(config_filename: Optional[str] = None) -> UserOptions:


def create_report(
user_options: UserOptions, show_timings: bool = False, verbose: bool = False
user_options: UserOptions,
limit_to_contracts: Tuple[str, ...] = tuple(),
show_timings: bool = False,
verbose: bool = False,
) -> Report:
"""
Analyse whether a Python package follows a set of contracts, returning a report on the results.
Expand All @@ -109,6 +114,7 @@ def create_report(
graph=graph,
graph_building_duration=graph_building_duration,
user_options=user_options,
limit_to_contracts=limit_to_contracts,
show_timings=show_timings,
verbose=verbose,
)
Expand Down Expand Up @@ -141,13 +147,17 @@ def _build_report(
graph: ImportGraph,
graph_building_duration: int,
user_options: UserOptions,
limit_to_contracts: Tuple[str, ...],
show_timings: bool,
verbose: bool,
) -> Report:
report = Report(
graph=graph, show_timings=show_timings, graph_building_duration=graph_building_duration
)
for contract_options in user_options.contracts_options:
contracts_options = _filter_contract_options(
user_options.contracts_options, limit_to_contracts
)
for contract_options in contracts_options:
contract_class = registry.get_contract_class(contract_options["type"])
try:
contract = contract_class(
Expand All @@ -173,6 +183,33 @@ def _build_report(
return report


def _filter_contract_options(
contracts_options: List[Dict[str, Any]], limit_to_contracts: Tuple[str, ...]
) -> List[Dict[str, Any]]:
if limit_to_contracts:
# Validate the supplied contract ids.
registered_contract_ids = {option["id"] for option in contracts_options}
missing_contract_ids = set(limit_to_contracts) - registered_contract_ids
if missing_contract_ids:
if len(missing_contract_ids) == 1:
raise ValueError(
f"Could not find contract '{missing_contract_ids.pop()}'.\n\n"
"You asked to limit the check to that contract, but nothing exists "
"with that id."
)
else:
raise ValueError(
"Could not find the following contract ids: "
f"{', '.join(sorted(missing_contract_ids))}.\n\n"
"You asked to limit the check to those contracts, but there are no "
"contracts with those ids."
)
else:
return [o for o in contracts_options if o["id"] in limit_to_contracts]
else:
return contracts_options


def _register_contract_types(user_options: UserOptions) -> None:
contract_types = _get_built_in_contract_types() + _get_plugin_contract_types(user_options)
for name, contract_class in contract_types:
Expand Down
38 changes: 27 additions & 11 deletions src/importlinter/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import sys
from typing import Optional
from typing import Optional, Tuple

import click

Expand All @@ -15,6 +15,12 @@

@click.command()
@click.option("--config", default=None, help="The config file to use.")
@click.option(
"--contract",
default=(),
multiple=True,
help="Limit the check to the supplied contract identifier. May be passed multiple times.",
)
@click.option("--debug", is_flag=True, help="Run in debug mode.")
@click.option(
"--show-timings",
Expand All @@ -27,19 +33,28 @@
help="Noisily output progress as we go along.",
)
def lint_imports_command(
config: Optional[str], debug: bool, show_timings: bool, verbose: bool
config: Optional[str],
contract: Tuple[str, ...],
debug: bool,
show_timings: bool,
verbose: bool,
) -> int:
"""
The entry point for the CLI command.
Check that a project adheres to a set of contracts.
"""
exit_code = lint_imports(
config_filename=config, is_debug_mode=debug, show_timings=show_timings, verbose=verbose
config_filename=config,
limit_to_contracts=contract,
is_debug_mode=debug,
show_timings=show_timings,
verbose=verbose,
)
sys.exit(exit_code)


def lint_imports(
config_filename: Optional[str] = None,
limit_to_contracts: Tuple[str, ...] = (),
is_debug_mode: bool = False,
show_timings: bool = False,
verbose: bool = False,
Expand All @@ -50,13 +65,13 @@ def lint_imports(
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.
is_debug_mode: Whether debugging should be turned on. In debug mode, exceptions are
not swallowed at the top level, so the stack trace can be seen.
show_timings: Whether to show the times taken to build the graph and to check
each contract.
verbose: If True, noisily output progress as we go along.
config_filename: the filename to use to parse user options.
limit_to_contracts: if supplied, only lint the contracts with the supplied ids.
is_debug_mode: whether debugging should be turned on. In debug mode, exceptions are
not swallowed at the top level, so the stack trace can be seen.
show_timings: whether to show the times taken to build the graph and to check
each contract.
verbose: if True, noisily output progress as it goes along.
Returns:
EXIT_STATUS_SUCCESS or EXIT_STATUS_ERROR.
Expand All @@ -66,6 +81,7 @@ def lint_imports(

passed = use_cases.lint_imports(
config_filename=config_filename,
limit_to_contracts=limit_to_contracts,
is_debug_mode=is_debug_mode,
show_timings=show_timings,
verbose=verbose,
Expand Down
2 changes: 1 addition & 1 deletion tests/assets/multipleroots/.multiplerootskeptcontract.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root_packages =
rootpackagegreen


[importlinter:contract:one]
[importlinter:contract:multiple-roots]
name=Multiple roots kept contract
type=forbidden
source_modules=rootpackageblue.one.alpha
Expand Down
2 changes: 1 addition & 1 deletion tests/assets/testpackage/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[importlinter]
root_package = testpackage

[importlinter:contract:one]
[importlinter:contract:test-independence]
name=Test independence contract
type=independence
modules=
Expand Down

0 comments on commit 58f797f

Please sign in to comment.