archunitpython

ArchUnitPython - Architecture testing library for Python projects.

 1"""ArchUnitPython - Architecture testing library for Python projects."""
 2
 3__version__ = "1.6.1"
 4
 5# Files API
 6# Common
 7from archunitpython.common import (
 8    CheckOptions,
 9    EmptyTestViolation,
10    TechnicalError,
11    UserError,
12    Violation,
13)
14from archunitpython.common.extraction import clear_graph_cache, extract_graph
15from archunitpython.config import ConfiguredRule, rules_from_config
16from archunitpython.files import files, project_files
17from archunitpython.graph import dependency_graph, project_graph
18from archunitpython.layers import layers, project_layers
19
20# Metrics API
21from archunitpython.metrics import metrics
22
23# Slices API
24from archunitpython.slices import project_slices
25
26# Testing
27from archunitpython.testing import assert_passes, format_violations
28
29__all__ = [
30    # Files
31    "project_files",
32    "files",
33    # Graph
34    "project_graph",
35    "dependency_graph",
36    # Layers
37    "project_layers",
38    "layers",
39    # Config
40    "rules_from_config",
41    "ConfiguredRule",
42    # Slices
43    "project_slices",
44    # Metrics
45    "metrics",
46    # Testing
47    "assert_passes",
48    "format_violations",
49    # Common
50    "Violation",
51    "EmptyTestViolation",
52    "CheckOptions",
53    "TechnicalError",
54    "UserError",
55    "extract_graph",
56    "clear_graph_cache",
57]
def project_files( project_path: str | None = None) -> archunitpython.files.fluentapi.files.FileConditionBuilder:
42def project_files(project_path: str | None = None) -> "FileConditionBuilder":
43    """Entry point for file-level architecture rules.
44
45    Args:
46        project_path: Root directory of the project to analyze.
47            Defaults to current working directory.
48    """
49    return FileConditionBuilder(project_path)

Entry point for file-level architecture rules.

Arguments:
  • project_path: Root directory of the project to analyze. Defaults to current working directory.
def files( project_path: str | None = None) -> archunitpython.files.fluentapi.files.FileConditionBuilder:
42def project_files(project_path: str | None = None) -> "FileConditionBuilder":
43    """Entry point for file-level architecture rules.
44
45    Args:
46        project_path: Root directory of the project to analyze.
47            Defaults to current working directory.
48    """
49    return FileConditionBuilder(project_path)

Entry point for file-level architecture rules.

Arguments:
  • project_path: Root directory of the project to analyze. Defaults to current working directory.
def project_graph( project_path: str | None = None) -> archunitpython.graph.graph_reporter.ProjectGraphBuilder:
107def project_graph(project_path: str | None = None) -> ProjectGraphBuilder:
108    """Create a builder for dependency graph reports."""
109    return ProjectGraphBuilder(project_path)

Create a builder for dependency graph reports.

def dependency_graph( project_path: str | None = None) -> archunitpython.graph.graph_reporter.ProjectGraphBuilder:
107def project_graph(project_path: str | None = None) -> ProjectGraphBuilder:
108    """Create a builder for dependency graph reports."""
109    return ProjectGraphBuilder(project_path)

Create a builder for dependency graph reports.

def project_layers( project_path: str | None = None) -> archunitpython.layers.fluentapi.layers.LayeredArchitecture:
19def project_layers(project_path: str | None = None) -> "LayeredArchitecture":
20    """Entry point for layer-level architecture rules."""
21    return LayeredArchitecture(project_path)

Entry point for layer-level architecture rules.

def layers( project_path: str | None = None) -> archunitpython.layers.fluentapi.layers.LayeredArchitecture:
19def project_layers(project_path: str | None = None) -> "LayeredArchitecture":
20    """Entry point for layer-level architecture rules."""
21    return LayeredArchitecture(project_path)

Entry point for layer-level architecture rules.

def rules_from_config(config_path: str) -> list[ConfiguredRule]:
30def rules_from_config(config_path: str) -> list[ConfiguredRule]:
31    """Load common architecture rules from a JSON config file.
32
33    The fluent Python API remains the primary interface. Config files provide a
34    lightweight way to share straightforward rules across projects or teams.
35    """
36    path = Path(config_path)
37    try:
38        raw_config = json.loads(path.read_text(encoding="utf-8"))
39    except OSError as exc:
40        raise UserError(f"Could not read config file: {config_path}") from exc
41    except json.JSONDecodeError as exc:
42        raise UserError(f"Invalid JSON config file: {config_path}") from exc
43
44    if not isinstance(raw_config, dict):
45        raise UserError("Architecture config must be a JSON object.")
46
47    project_path = _optional_string(raw_config, "project_path") or os.getcwd()
48    rules = raw_config.get("rules")
49    if not isinstance(rules, list):
50        raise UserError("Architecture config must define a 'rules' list.")
51
52    base_dir = str(path.parent if path.parent != Path("") else Path.cwd())
53    resolved_project_path = _resolve_project_path(base_dir, project_path)
54
55    return [_build_rule(resolved_project_path, item, index) for index, item in enumerate(rules, 1)]

Load common architecture rules from a JSON config file.

The fluent Python API remains the primary interface. Config files provide a lightweight way to share straightforward rules across projects or teams.

@dataclass(frozen=True)
class ConfiguredRule:
18@dataclass(frozen=True)
19class ConfiguredRule:
20    """A named rule loaded from a configuration file."""
21
22    name: str
23    rule: Checkable
24
25    def check(self, options: CheckOptions | None = None) -> list[Violation]:
26        """Run the configured rule."""
27        return self.rule.check(options)

A named rule loaded from a configuration file.

ConfiguredRule(name: str, rule: archunitpython.common.fluentapi.checkable.Checkable)
name: str
rule: archunitpython.common.fluentapi.checkable.Checkable
def check( self, options: CheckOptions | None = None) -> list[Violation]:
25    def check(self, options: CheckOptions | None = None) -> list[Violation]:
26        """Run the configured rule."""
27        return self.rule.check(options)

Run the configured rule.

def project_slices( project_path: str | None = None) -> archunitpython.slices.fluentapi.slices.SliceConditionBuilder:
33def project_slices(project_path: str | None = None) -> "SliceConditionBuilder":
34    """Entry point for slice-level architecture rules.
35
36    Args:
37        project_path: Root directory of the project to analyze.
38    """
39    return SliceConditionBuilder(project_path)

Entry point for slice-level architecture rules.

Arguments:
  • project_path: Root directory of the project to analyze.
def metrics( project_path: str | None = None) -> archunitpython.metrics.fluentapi.metrics.MetricsBuilder:
53def metrics(project_path: str | None = None) -> "MetricsBuilder":
54    """Entry point for metrics rules."""
55    return MetricsBuilder(project_path)

Entry point for metrics rules.

def assert_passes( checkable: archunitpython.common.fluentapi.checkable.Checkable, options: CheckOptions | None = None) -> None:
40def assert_passes(
41    checkable: Checkable,
42    options: CheckOptions | None = None,
43) -> None:
44    """Assert that an architecture rule passes (no violations).
45
46    Args:
47        checkable: Any object with a check() method (implements Checkable).
48        options: Optional check options.
49
50    Raises:
51        AssertionError: If the rule has violations.
52    """
53    violations = checkable.check(options)
54    if violations:
55        because = getattr(checkable, "because_reason", None)
56        raise AssertionError(format_violations(violations, because=because))

Assert that an architecture rule passes (no violations).

Arguments:
  • checkable: Any object with a check() method (implements Checkable).
  • options: Optional check options.
Raises:
  • AssertionError: If the rule has violations.
def format_violations( violations: list[Violation], *, because: str | None = None) -> str:
11def format_violations(
12    violations: list[Violation],
13    *,
14    because: str | None = None,
15) -> str:
16    """Format violations into a human-readable string.
17
18    Args:
19        violations: List of violations to format.
20
21    Returns:
22        Formatted string describing all violations.
23    """
24    if not violations:
25        return "No violations found."
26
27    lines = [f"Found {len(violations)} architecture violation(s):"]
28    if because:
29        lines.extend(["", f"Because: {because}"])
30    lines.append("")
31    for i, violation in enumerate(violations, 1):
32        tv = ViolationFactory.from_violation(violation)
33        lines.append(f"  {i}. {tv.message}")
34        lines.append(f"     {tv.details}")
35        lines.append("")
36
37    return "\n".join(lines)

Format violations into a human-readable string.

Arguments:
  • violations: List of violations to format.
Returns:

Formatted string describing all violations.

class Violation:
10class Violation:
11    """Base class for all architecture violations."""
12
13    pass

Base class for all architecture violations.

@dataclass
class EmptyTestViolation(archunitpython.Violation):
16@dataclass
17class EmptyTestViolation(Violation):
18    """Violation raised when no files match the specified filter patterns."""
19
20    filters: list[Any]
21    message: str
22    is_negated: bool = False

Violation raised when no files match the specified filter patterns.

EmptyTestViolation(filters: list[typing.Any], message: str, is_negated: bool = False)
filters: list[typing.Any]
message: str
is_negated: bool = False
@dataclass(frozen=True)
class CheckOptions:
13@dataclass(frozen=True)
14class CheckOptions:
15    """Options for controlling rule check execution."""
16
17    allow_empty_tests: bool = False
18    logging: LoggingOptions | None = None
19    clear_cache: bool = False
20    ignore_type_checking_imports: bool = False

Options for controlling rule check execution.

CheckOptions( allow_empty_tests: bool = False, logging: archunitpython.common.logging.types.LoggingOptions | None = None, clear_cache: bool = False, ignore_type_checking_imports: bool = False)
allow_empty_tests: bool = False
logging: archunitpython.common.logging.types.LoggingOptions | None = None
clear_cache: bool = False
ignore_type_checking_imports: bool = False
class TechnicalError(builtins.Exception):
5class TechnicalError(Exception):
6    """Error caused by technical issues (file system, configuration, etc.)."""
7
8    pass

Error caused by technical issues (file system, configuration, etc.).

class UserError(builtins.Exception):
11class UserError(Exception):
12    """Error caused by incorrect API usage."""
13
14    pass

Error caused by incorrect API usage.

def extract_graph( project_path: str | None = None, *, exclude_patterns: list[str] | None = None, options: CheckOptions | None = None) -> list[archunitpython.common.extraction.graph.Edge]:
 72def extract_graph(
 73    project_path: str | None = None,
 74    *,
 75    exclude_patterns: list[str] | None = None,
 76    options: CheckOptions | None = None,
 77) -> Graph:
 78    """Extract a dependency graph from a Python project.
 79
 80    Scans all .py files in the project directory, parses their imports,
 81    and resolves them to build a list of Edge objects.
 82
 83    Args:
 84        project_path: Root directory of the project to analyze.
 85            Defaults to current working directory.
 86        exclude_patterns: Directory/file names to exclude.
 87            Defaults to common non-source directories.
 88        options: Check options (supports clear_cache).
 89
 90    Returns:
 91        List of Edge objects representing import relationships.
 92    """
 93    if project_path is None:
 94        project_path = os.getcwd()
 95
 96    project_path = os.path.abspath(project_path)
 97    excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
 98    ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
 99    cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
100
101    if options and options.clear_cache:
102        _graph_cache.pop(cache_key, None)
103
104    if cache_key in _graph_cache:
105        return _graph_cache[cache_key]
106
107    result = _extract_graph_uncached(
108        project_path,
109        excludes,
110        ignore_type_checking_imports=ignore_type_checking_imports,
111    )
112    _graph_cache[cache_key] = result
113    return result

Extract a dependency graph from a Python project.

Scans all .py files in the project directory, parses their imports, and resolves them to build a list of Edge objects.

Arguments:
  • project_path: Root directory of the project to analyze. Defaults to current working directory.
  • exclude_patterns: Directory/file names to exclude. Defaults to common non-source directories.
  • options: Check options (supports clear_cache).
Returns:

List of Edge objects representing import relationships.

def clear_graph_cache( options: CheckOptions | None = None) -> None:
67def clear_graph_cache(options: CheckOptions | None = None) -> None:
68    """Clear the cached dependency graphs."""
69    _graph_cache.clear()

Clear the cached dependency graphs.