Skip to content

Commit

Permalink
Add verbosity flag
Browse files Browse the repository at this point in the history
  • Loading branch information
seddonym committed Sep 29, 2022
1 parent 77b79ad commit 3e176b7
Show file tree
Hide file tree
Showing 20 changed files with 688 additions and 156 deletions.
2 changes: 1 addition & 1 deletion .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ containers =
layers=
cli
api
contracts
configuration
adapters
contracts
application
domain
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ latest
* Include py.typed file in package data to support type checking
* Remove upper bounds on dependencies. This allows usage of Grimp 2.0, which should significantly speed up checking of
layers contracts.
* Add --verbose flag to lint-imports command.

1.3.0 (2022-08-22)
------------------
Expand Down
14 changes: 10 additions & 4 deletions docs/custom_contract_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ Step one: implementing a Contract class
You define a custom contract type by subclassing ``importlinter.Contract`` and implementing the
following methods:

- ``check(graph)``:
- ``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 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`_.
- ``verbose``: Whether we're in :ref:`verbose mode <verbose-mode>`. You can use this flag to determine whether to output text
during the check, using ``output.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) 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)``:
- ``render_broken_contract(check: ContractCheck) -> None``:

Renders the results of a broken contract check. For output, this should use the
``importlinter.output`` module.
Expand Down Expand Up @@ -56,7 +58,11 @@ see ``importlinter.contracts.layers``.
importer = fields.StringField()
imported = fields.StringField()
def check(self, graph):
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,
Expand Down Expand Up @@ -109,4 +115,4 @@ You may now use the type name defined in the previous step to define a contract:
importer = mypackage.foo
imported = mypackage.bar
.. _Grimp documentation: https://grimp.readthedocs.io
.. _Grimp documentation: https://grimp.readthedocs.io
17 changes: 14 additions & 3 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,13 @@ Running this will check that your project adheres to the contracts you've define
**Arguments:**

- ``--config``:
(optional) The configuration file to use. This overrides the default file search strategy.
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``.
- ``show_timings``:
Display the times taken to build the graph and check each contract.
(Optional.)
- ``--show_timings``:
Display the times taken to build the graph and check each contract. (Optional.)
- ``--verbose``:
Noisily output progress as it goes along. (Optional.)

**Default usage:**

Expand All @@ -117,5 +120,13 @@ Running this will check that your project adheres to the contracts you've define
lint-imports --show-timings
.. _verbose-mode:

**Verbose mode:**

.. code-block:: text
lint-imports --verbose
.. _namespace packages: https://docs.python.org/3/glossary.html#term-namespace-package
.. _portion: https://docs.python.org/3/glossary.html#term-portion
15 changes: 15 additions & 0 deletions src/importlinter/application/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,18 @@ def printer(self) -> Printer:
print_heading = _instance.print_heading
print_error = _instance.print_error
print_warning = _instance.print_warning


def verbose_print(
verbose: bool,
text: str = "",
bold: bool = False,
color: Optional[str] = None,
newline: bool = True,
) -> None:
"""
Print a message, but only if we're in verbose mode.
"""
if verbose:
printer: Printer = settings.PRINTER
printer.print(text, bold, color, newline)
11 changes: 8 additions & 3 deletions src/importlinter/application/ports/timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ class Timer(abc.ABC):
print(timer.duration_in_s)
"""

def __init__(self) -> None:
# We use a stack so context managers can be nested.
self._start_stack: list[float] = []

def __enter__(self) -> Timer:
self.start = self.get_current_time()
self._start_stack.append(self.get_current_time())
return self

def __exit__(
Expand All @@ -28,8 +32,9 @@ def __exit__(
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.end = self.get_current_time()
self.duration_in_s = int(self.end - self.start)
end = self.get_current_time()
start = self._start_stack.pop()
self.duration_in_s = int(end - start)

@abc.abstractmethod
def get_current_time(self) -> float:
Expand Down
41 changes: 29 additions & 12 deletions src/importlinter/application/rendering.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from typing import Optional

from importlinter.domain.contract import Contract, ContractCheck

from . import output
from .ports.reporting import Report

Expand All @@ -13,8 +17,6 @@ def render_report(report: Report) -> None:
_render_could_not_run(report)
return

output.print_heading("Import Linter", output.HEADING_LEVEL_ONE)

if report.show_timings:
output.print(f"Building graph took {report.graph_building_duration}s.")
output.new_line()
Expand All @@ -28,16 +30,8 @@ def render_report(report: Report) -> None:
)

for contract, contract_check in report.get_contracts_and_checks():
result_text = "KEPT" if contract_check.kept else "BROKEN"
warning_text = _build_warning_text(warnings_count=len(contract_check.warnings))
color_key = output.SUCCESS if contract_check.kept else output.ERROR
color = output.COLORS[color_key]
output.print(f"{contract.name} ", newline=False)
output.print(result_text, color=color, newline=False)
output.print(warning_text, color=output.COLORS[output.WARNING], newline=False)
if report.show_timings:
output.print(f" [{report.get_duration(contract)}s]", newline=False)
output.new_line()
duration = report.get_duration(contract) if report.show_timings else None
render_contract_result_line(contract, contract_check, duration=duration)

output.new_line()

Expand All @@ -53,6 +47,29 @@ def render_report(report: Report) -> None:
_render_broken_contracts_details(report)


def render_contract_result_line(
contract: Contract, contract_check: ContractCheck, duration: Optional[int]
) -> None:
"""
Render the one-line contract check result.
Args:
...
duration: The number of seconds the contract took to check (optional).
The duration will only be displayed if it is provided.
"""
result_text = "KEPT" if contract_check.kept else "BROKEN"
warning_text = _build_warning_text(warnings_count=len(contract_check.warnings))
color_key = output.SUCCESS if contract_check.kept else output.ERROR
color = output.COLORS[color_key]
output.print(f"{contract.name} ", newline=False)
output.print(result_text, color=color, newline=False)
output.print(warning_text, color=output.COLORS[output.WARNING], newline=False)
if duration is not None:
output.print(f" [{duration}s]", newline=False)
output.new_line()


def render_exception(exception: Exception) -> None:
"""
Render any exception to the console.
Expand Down
32 changes: 27 additions & 5 deletions src/importlinter/application/use_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from copy import copy, deepcopy
from typing import List, Optional, Tuple, Type

from ..application import rendering
from ..domain.contract import Contract, InvalidContractOptions, registry
from ..domain.ports.graph import ImportGraph
from . import output
from .app_config import settings
from .ports.reporting import Report
from .rendering import render_exception, render_report
Expand All @@ -17,7 +19,10 @@


def lint_imports(
config_filename: Optional[str] = None, is_debug_mode: bool = False, show_timings: bool = False
config_filename: Optional[str] = None,
is_debug_mode: bool = False,
show_timings: bool = False,
verbose: bool = False,
) -> bool:
"""
Analyse whether a Python package follows a set of contracts, and report on the results.
Expand All @@ -30,14 +35,17 @@ def lint_imports(
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.
"""
output.print_heading("Import Linter", output.HEADING_LEVEL_ONE)
output.verbose_print(verbose, "Verbose mode.")
try:
user_options = read_user_options(config_filename=config_filename)
_register_contract_types(user_options)
report = create_report(user_options, show_timings)
report = create_report(user_options, show_timings, verbose)
except Exception as e:
if is_debug_mode:
raise e
Expand Down Expand Up @@ -77,7 +85,9 @@ def read_user_options(config_filename: Optional[str] = None) -> UserOptions:
raise FileNotFoundError("Could not read any configuration.")


def create_report(user_options: UserOptions, show_timings: bool = False) -> Report:
def create_report(
user_options: UserOptions, 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 @@ -86,18 +96,21 @@ def create_report(user_options: UserOptions, show_timings: bool = False) -> Repo
such as a module that could not be imported.
"""
include_external_packages = _get_include_external_packages(user_options)
output.verbose_print(verbose, "Building import graph...")
with settings.TIMER as timer:
graph = _build_graph(
root_package_names=user_options.session_options["root_packages"],
include_external_packages=include_external_packages,
)
graph_building_duration = timer.duration_in_s
output.verbose_print(verbose, f"Built graph in {graph_building_duration}s.")

return _build_report(
graph=graph,
graph_building_duration=graph_building_duration,
user_options=user_options,
show_timings=show_timings,
verbose=verbose,
)


Expand Down Expand Up @@ -125,7 +138,11 @@ def _build_graph(


def _build_report(
graph: ImportGraph, graph_building_duration: int, user_options: UserOptions, show_timings: bool
graph: ImportGraph,
graph_building_duration: int,
user_options: UserOptions,
show_timings: bool,
verbose: bool,
) -> Report:
report = Report(
graph=graph, show_timings=show_timings, graph_building_duration=graph_building_duration
Expand All @@ -142,12 +159,17 @@ def _build_report(
report.add_invalid_contract_options(contract_options["name"], e)
return report

output.verbose_print(verbose, f"Checking {contract.name}...")
with settings.TIMER as timer:
# Make a copy so that contracts can mutate the graph without affecting
# other contract checks.
copy_of_graph = deepcopy(graph)
check = contract.check(copy_of_graph)
check = contract.check(copy_of_graph, verbose=verbose)
report.add_contract_check(contract, check, duration=timer.duration_in_s)
if verbose:
rendering.render_contract_result_line(contract, check, duration=timer.duration_in_s)

output.verbose_print(verbose, newline=True)
return report


Expand Down
22 changes: 18 additions & 4 deletions src/importlinter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,28 @@
is_flag=True,
help="Show times taken to build the graph and to check each contract.",
)
def lint_imports_command(config: Optional[str], debug: bool, show_timings: bool) -> int:
@click.option(
"--verbose",
is_flag=True,
help="Noisily output progress as we go along.",
)
def lint_imports_command(
config: Optional[str], debug: bool, show_timings: bool, verbose: bool
) -> int:
"""
The entry point for the CLI command.
"""
exit_code = lint_imports(
config_filename=config, is_debug_mode=debug, show_timings=show_timings
config_filename=config, is_debug_mode=debug, show_timings=show_timings, verbose=verbose
)
sys.exit(exit_code)


def lint_imports(
config_filename: Optional[str] = None, is_debug_mode: bool = False, show_timings: bool = False
config_filename: Optional[str] = None,
is_debug_mode: bool = False,
show_timings: bool = False,
verbose: bool = False,
) -> int:
"""
Check that a project adheres to a set of contracts.
Expand All @@ -46,6 +56,7 @@ def lint_imports(
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.
Returns:
EXIT_STATUS_SUCCESS or EXIT_STATUS_ERROR.
Expand All @@ -54,7 +65,10 @@ def lint_imports(
sys.path.insert(0, os.getcwd())

passed = use_cases.lint_imports(
config_filename=config_filename, is_debug_mode=is_debug_mode, show_timings=show_timings
config_filename=config_filename,
is_debug_mode=is_debug_mode,
show_timings=show_timings,
verbose=verbose,
)

if passed:
Expand Down

0 comments on commit 3e176b7

Please sign in to comment.