Intermediate3h

Python OOP Content Review CLI Project

Build a CLI tool that audits Python files for OOP best practices — encapsulation, inheritance, polymorphism, and SOLID principles.

Overview

In this project, you'll build pyreview — a command-line tool that scans Python source files and evaluates them against object-oriented design best practices. The tool checks for encapsulation leaks, inheritance misuse, missing abstract base classes, god classes, and other common OOP anti-patterns.

This project reinforces real-world skills: reading and analyzing code, applying design principles, working with ASTs (Abstract Syntax Trees), and building polished CLI tools with argparse and rich terminal output.

What You'll Learn

  • Parsing Python code with the ast module
  • Detecting OOP anti-patterns programmatically
  • Building CLI tools with argparse and rich
  • Writing tests for code analysis tools
  • Packaging and distributing Python CLI tools

Interactive Learning Experience

This project is built to take full advantage of our learning platform's interactive capabilities:

  1. In-Browser Sandbox (WebContainers): Write and run python code inside our fully-featured WebContainer workspace. It boots quickly in the browser, offering an interactive coding environment. If the sandbox encounters environment issues (such as lacking cross-origin isolation headers), a fallback notice is displayed with instructions to download files and run locally.
  2. Video Lessons & Custom Player: Follow video walkthroughs matching each milestone. The video player includes interactive chapter controls, speed adjustments, and a collapsible real-time transcript panel below the player.
  3. Interactive Task Checklist: Check off items as you finish coding requirements. A dynamic progress panel tracks your completion percentage.
  4. Premium Solution Gate: While the project overview and starter files are free, the complete reference code, unit tests, and solution markdown are gated for Premium users.
  5. AI Code Review & Submission: Once complete, submit your GitHub repository. The platform queues your submission for an automated code review, scoring your code and providing detailed feedback on strengths and refactoring opportunities.
  6. Video Compile Progress (Admin): When administrators trigger or update lesson videos, a real-time progress modal displays step-by-step video compilation states (voice synthesis, slide rendering, ffmpeg muxing, and auto-revalidation).

Starter Knowledge

  • Decorators and @property
  • Abstract Base Classes (abc module)
  • Type hints
  • Writing unit tests with pytest

Project Structure

text
pyreview/
├── pyreview/
│   ├── __init__.py
│   ├── __main__.py
│   ├── cli.py           # CLI entry point with argparse
│   ├── analyzer.py      # Core analysis engine
│   ├── checks/
│   │   ├── __init__.py
│   │   ├── encapsulation.py
│   │   ├── inheritance.py
│   │   ├── cohesion.py
│   │   └── naming.py
│   ├── ast_helpers.py   # AST utility functions
│   └── reporter.py      # Output formatting with rich
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_analyzer.py
│   ├── test_encapsulation.py
│   ├── test_inheritance.py
│   ├── test_cohesion.py
│   └── fixtures/
│       ├── good_class.py
│       ├── bad_encapsulation.py
│       ├── bad_inheritance.py
│       └── god_class.py
├── pyproject.toml
├── README.md
└── .gitignore

Milestones

  1. Setup and scaffolding — project structure, CLI skeleton, argparse
  2. AST analysis engine — parse Python files, walk AST nodes
  3. Encapsulation checks — detect public attributes, missing properties
  4. Inheritance checks — deep hierarchies, missing abstract methods
  5. Cohesion and naming checks — god classes, method naming conventions
  6. Reporter and formatting — rich tables, color-coded output, severity levels
  7. Tests and fixtures — test against known good/bad example files
  8. Polish and package — pyproject.toml, console_scripts entry point, README

Step 2: CLI Entry Point

Create pyreview/cli.py using argparse:

python
"""pyreview CLI — Audit Python OOP code for best practices."""
import argparse
import sys
from pathlib import Path

from pyreview.analyzer import Analyzer
from pyreview.reporter import format_report

def main() -> None:
    parser = argparse.ArgumentParser(
        prog="pyreview",
        description="Audit Python files for OOP design best practices.",
    )
    parser.add_argument(
        "path",
        type=Path,
        help="Python file or directory to analyze",
    )
    parser.add_argument(
        "--severity",
        choices=["info", "warning", "error"],
        default="warning",
        help="Minimum severity level to report (default: warning)",
    )
    parser.add_argument(
        "--format",
        choices=["rich", "json", "summary"],
        default="rich",
        help="Output format (default: rich)",
    )
    parser.add_argument(
        "--checks",
        nargs="+",
        choices=["encapsulation", "inheritance", "cohesion", "naming", "all"],
        default=["all"],
        help="Specific checks to run (default: all)",
    )

    args = parser.parse_args()

    if not args.path.exists():
        print(f"Error: '{args.path}' does not exist.", file=sys.stderr)
        sys.exit(1)

    analyzer = Analyzer(severity=args.severity, checks=args.checks)
    results = analyzer.analyze(args.path)
    format_report(results, fmt=args.format)

if __name__ == "__main__":
    main()

Step 3: AST Analysis Engine

Create pyreview/analyzer.py — the core engine that orchestrates checks:

python
"""Core analysis engine that runs checks against Python source files."""
from __future__ import annotations

import ast
from pathlib import Path
from dataclasses import dataclass, field

from pyreview.checks.encapsulation import check_encapsulation
from pyreview.checks.inheritance import check_inheritance
from pyreview.checks.cohesion import check_cohesion
from pyreview.checks.naming import check_naming

@dataclass
class Issue:
    """A single code issue found during analysis."""
    file: str
    line: int
    check: str
    severity: str  # info, warning, error
    message: str
    suggestion: str = ""

@dataclass
class AnalysisResult:
    """Aggregated results from running checks."""
    file: str
    issues: list[Issue] = field(default_factory=list)

    @property
    def error_count(self) -> int:
        return sum(1 for i in self.issues if i.severity == "error")

    @property
    def warning_count(self) -> int:
        return sum(1 for i in self.issues if i.severity == "warning")

    @property
    def info_count(self) -> int:
        return sum(1 for i in self.issues if i.severity == "info")

class Analyzer:
    """Runs OOP best-practice checks against Python source files."""

    SEVERITY_ORDER = {"error": 0, "warning": 1, "info": 2}

    def __init__(self, severity: str = "warning", checks: list[str] | None = None) -> None:
        self.min_severity = severity
        self.active_checks = checks or ["all"]
        self._run_all = "all" in self.active_checks

    def _should_run(self, check_name: str) -> bool:
        return self._run_all or check_name in self.active_checks

    def _passes_severity(self, issue_severity: str) -> bool:
        return self.SEVERITY_ORDER[issue_severity] <= self.SEVERITY_ORDER[self.min_severity]

    def analyze(self, path: Path) -> list[AnalysisResult]:
        """Analyze a file or directory, returning results for each Python file."""
        if path.is_file():
            return [self._analyze_file(path)]
        return [self._analyze_file(f) for f in path.rglob("*.py") if f.is_file()]

    def _analyze_file(self, filepath: Path) -> AnalysisResult:
        source = filepath.read_text(encoding="utf-8")
        tree = ast.parse(source, filename=str(filepath))

        result = AnalysisResult(file=str(filepath))

        checks_to_run = [
            (check_encapsulation, "encapsulation"),
            (check_inheritance, "inheritance"),
            (check_cohesion, "cohesion"),
            (check_naming, "naming"),
        ]

        for check_fn, check_name in checks_to_run:
            if not self._should_run(check_name):
                continue
            for issue in check_fn(tree, str(filepath)):
                if self._passes_severity(issue.severity):
                    result.issues.append(issue)

        return result

Step 4: Encapsulation Check

Create pyreview/checks/encapsulation.py:

python
"""Checks for encapsulation violations in Python classes."""
from __future__ import annotations

import ast

from pyreview.analyzer import Issue

def check_encapsulation(tree: ast.AST, filepath: str) -> list[Issue]:
    """Detect encapsulation issues: public mutable attributes, missing @property, direct _private access."""
    issues: list[Issue] = []

    for node in ast.walk(tree):
        if not isinstance(node, ast.ClassDef):
            continue

        public_attrs: list[str] = []
        has_init = False

        for item in node.body:
            # Check __init__ for public attribute assignments
            if isinstance(item, ast.FunctionDef) and item.name == "__init__":
                has_init = True
                for stmt in ast.walk(item):
                    if isinstance(stmt, ast.Assign):
                        for target in stmt.targets:
                            if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name):
                                if target.value.id == "self" and not target.attr.startswith("_"):
                                    public_attrs.append(target.attr)

        # Warn about classes with many public attributes (potential data class / god object)
        if len(public_attrs) > 5:
            issues.append(Issue(
                file=filepath,
                line=node.lineno,
                check="encapsulation",
                severity="warning",
                message=f"Class '{node.name}' has {len(public_attrs)} public attributes. "
                        f"Consider grouping related attributes into smaller classes or using @dataclass.",
                suggestion=f"Break '{node.name}' into smaller, focused classes or use composition.",
            ))

        # Detect direct access to _private attributes outside the defining class
        # (simplified: flag any _prefixed attribute access not on 'self')
        for child in ast.walk(node):
            if isinstance(child, ast.Attribute) and child.attr.startswith("_") and not child.attr.startswith("__"):
                if isinstance(child.value, ast.Name) and child.value.id != "self":
                    issues.append(Issue(
                        file=filepath,
                        line=child.lineno,
                        check="encapsulation",
                        severity="warning",
                        message=f"Accessing private attribute '{child.attr}' from outside the class. "
                                f"Use a public method or @property instead.",
                        suggestion="Add a public method or @property to expose the needed behavior.",
                    ))

    return issues

Step 5: Inheritance Check

Create pyreview/checks/inheritance.py:

python
"""Checks for inheritance issues in Python class hierarchies."""
from __future__ import annotations

import ast

from pyreview.analyzer import Issue

MAX_INHERITANCE_DEPTH = 3

def check_inheritance(tree: ast.AST, filepath: str) -> list[Issue]:
    """Detect deep inheritance chains, missing abstract methods, and LSP violations."""
    issues: list[Issue] = []

    for node in ast.walk(tree):
        if not isinstance(node, ast.ClassDef):
            continue

        # Check inheritance depth
        depth = _inheritance_depth(node, tree)
        if depth > MAX_INHERITANCE_DEPTH:
            issues.append(Issue(
                file=filepath,
                line=node.lineno,
                check="inheritance",
                severity="warning",
                message=f"Class '{node.name}' has inheritance depth {depth} "
                        f"(max recommended: {MAX_INHERITANCE_DEPTH}). "
                        f"Deep hierarchies are hard to maintain — prefer composition.",
                suggestion="Refactor using composition or mixins instead of deep inheritance.",
            ))

        # Check for empty overrides (child overrides parent but just calls super())
        for item in node.body:
            if isinstance(item, ast.FunctionDef) and item.name != "__init__":
                if _is_empty_override(item):
                    issues.append(Issue(
                        file=filepath,
                        line=item.lineno,
                        check="inheritance",
                        severity="info",
                        message=f"Method '{item.name}' in '{node.name}' overrides parent "
                                f"but only calls super(). Remove the override if it adds no behavior.",
                        suggestion="Remove the empty override or add meaningful behavior.",
                    ))

        # Flag classes that inherit from multiple concrete classes
        bases = [b for b in node.bases if isinstance(b, ast.Name)]
        concrete_bases = _count_concrete_bases(bases)
        if concrete_bases > 1:
            issues.append(Issue(
                file=filepath,
                line=node.lineno,
                check="inheritance",
                severity="info",
                message=f"Class '{node.name}' inherits from {concrete_bases} concrete classes. "
                        f"Multiple inheritance can cause MRO confusion.",
                suggestion="Consider using mixins (abstract, no __init__) for additional behavior.",
            ))

    return issues

def _inheritance_depth(cls: ast.ClassDef, tree: ast.AST, current_depth: int = 0) -> int:
    """Recursively compute inheritance depth."""
    max_depth = current_depth
    for base in cls.bases:
        if isinstance(base, ast.Name):
            parent = _find_class(tree, base.id)
            if parent:
                depth = _inheritance_depth(parent, tree, current_depth + 1)
                max_depth = max(max_depth, depth)
    return max_depth

def _find_class(tree: ast.AST, name: str) -> ast.ClassDef | None:
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef) and node.name == name:
            return node
    return None

def _is_empty_override(func: ast.FunctionDef) -> bool:
    """Check if a method body only calls super()."""
    body = func.body
    if len(body) == 1 and isinstance(body[0], ast.Expr):
        call = body[0].value
        if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute):
            if isinstance(call.func.value, ast.Call) and isinstance(call.func.value.func, ast.Name):
                return call.func.value.func.id == "super"
    return False

def _count_concrete_bases(bases: list[ast.Name]) -> int:
    """Count bases that look like concrete classes (not ABC, Protocol, etc.)."""
    abstract_names = {"ABC", "Protocol", "object", "Exception", "BaseException"}
    return sum(1 for b in bases if b.id not in abstract_names)

Step 6: Cohesion Check

Create pyreview/checks/cohesion.py:

python
"""Checks for class cohesion — god classes, unrelated methods, etc."""
from __future__ import annotations

import ast

from pyreview.analyzer import Issue

MAX_METHODS_PER_CLASS = 15
MAX_PUBLIC_METHODS = 10

def check_cohesion(tree: ast.AST, filepath: str) -> list[Issue]:
    """Detect god classes, low cohesion, and classes that do too much."""
    issues: list[Issue] = []

    for node in ast.walk(tree):
        if not isinstance(node, ast.ClassDef):
            continue

        methods = [item for item in node.body if isinstance(item, ast.FunctionDef)]
        public_methods = [m for m in methods if not m.name.startswith("_")]
        total_lines = sum(
            m.end_lineno - m.lineno + 1
            for m in methods
            if m.end_lineno is not None
        )

        # God class: too many methods or too many lines
        if len(methods) > MAX_METHODS_PER_CLASS:
            issues.append(Issue(
                file=filepath,
                line=node.lineno,
                check="cohesion",
                severity="warning",
                message=f"Class '{node.name}' has {len(methods)} methods "
                        f"(max recommended: {MAX_METHODS_PER_CLASS}). "
                        f"This may be a 'God Class' with too many responsibilities.",
                suggestion="Split the class into smaller, single-responsibility classes.",
            ))

        # Too many public methods
        if len(public_methods) > MAX_PUBLIC_METHODS:
            issues.append(Issue(
                file=filepath,
                line=node.lineno,
                check="cohesion",
                severity="info",
                message=f"Class '{node.name}' has {len(public_methods)} public methods. "
                        f"Consider if some can be made private or extracted.",
                suggestion="Make internal helpers private (_method) or extract to a utility module.",
            ))

        # Very large class by line count
        if total_lines > 300:
            issues.append(Issue(
                file=filepath,
                line=node.lineno,
                check="cohesion",
                severity="warning",
                message=f"Class '{node.name}' is {total_lines} lines long. "
                        f"Large classes are hard to test and maintain.",
                suggestion="Extract related behavior into separate classes using composition.",
            ))

    return issues

Step 7: Naming Check

Create pyreview/checks/naming.py:

python
"""Checks for naming convention violations in OOP code."""
from __future__ import annotations

import ast
import re

from pyreview.analyzer import Issue

# Python naming conventions (PEP 8)
CLASS_NAME_RE = re.compile(r"^[A-Z][a-zA-Z0-9]*$")
METHOD_NAME_RE = re.compile(r"^[a-z_][a-z0-9_]*$")
CONSTANT_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
BOOLEAN_METHOD_PREFIXES = ("is_", "has_", "can_", "should_", "will_", "does_")

def check_naming(tree: ast.AST, filepath: str) -> list[Issue]:
    """Detect naming convention violations (PEP 8)."""
    issues: list[Issue] = []

    for node in ast.walk(tree):
        # Class names should be PascalCase
        if isinstance(node, ast.ClassDef):
            if not CLASS_NAME_RE.match(node.name):
                issues.append(Issue(
                    file=filepath,
                    line=node.lineno,
                    check="naming",
                    severity="error",
                    message=f"Class '{node.name}' does not follow PascalCase naming convention.",
                    suggestion=f"Rename to PascalCase, e.g., '{_to_pascal(node.name)}'.",
                ))

        # Method names should be snake_case
        if isinstance(node, ast.FunctionDef):
            # Skip dunder methods and test methods
            if node.name.startswith("__") and node.name.endswith("__"):
                continue
            if node.name.startswith("test_"):
                continue

            if not METHOD_NAME_RE.match(node.name):
                issues.append(Issue(
                    file=filepath,
                    line=node.lineno,
                    check="naming",
                    severity="error",
                    message=f"Method '{node.name}' does not follow snake_case naming convention.",
                    suggestion=f"Rename to snake_case, e.g., '{_to_snake(node.name)}'.",
                ))

        # Boolean-returning methods should start with is_/has_/can_
        if isinstance(node, ast.FunctionDef):
            if _returns_bool(node) and not node.name.startswith(BOOLEAN_METHOD_PREFIXES):
                issues.append(Issue(
                    file=filepath,
                    line=node.lineno,
                    check="naming",
                    severity="info",
                    message=f"Method '{node.name}' appears to return a boolean. "
                            f"Consider prefixing with is_/has_/can_ for clarity.",
                    suggestion=f"Rename to 'is_{node.name}' or 'has_{node.name}'.",
                ))

    return issues

def _returns_bool(func: ast.FunctionDef) -> bool:
    """Heuristic: check if function body contains 'return True' or 'return False'."""
    for node in ast.walk(func):
        if isinstance(node, ast.Return) and node.value is not None:
            if isinstance(node.value, ast.NameConstant):
                return isinstance(node.value.value, bool)
            if isinstance(node.value, ast.Constant):
                return isinstance(node.value.value, bool)
    return False

def _to_pascal(name: str) -> str:
    return "".join(word.capitalize() for word in re.split(r"[_\\s-]+", name) if word)

def _to_snake(name: str) -> str:
    result = re.sub(r"([A-Z])", r"_\1", name).lower().strip("_")
    return re.sub(r"_+", "_", result)

Step 8: Reporter with Rich

Create pyreview/reporter.py for beautiful terminal output:

python
"""Output formatting for analysis results using rich."""
from __future__ import annotations

import json
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.text import Text
from rich import box

from pyreview.analyzer import AnalysisResult

console = Console()

SEVERITY_COLORS = {
    "error": "bold red",
    "warning": "bold yellow",
    "info": "dim cyan",
}

SEVERITY_ICONS = {
    "error": "❌",
    "warning": "⚠️ ",
    "info": "ℹ️ ",
}

def format_report(results: list[AnalysisResult], fmt: str = "rich") -> None:
    """Format and print analysis results."""
    if fmt == "json":
        _format_json(results)
    elif fmt == "summary":
        _format_summary(results)
    else:
        _format_rich(results)

def _format_rich(results: list[AnalysisResult]) -> None:
    total_errors = sum(r.error_count for r in results)
    total_warnings = sum(r.warning_count for r in results)
    total_info = sum(r.info_count for r in results)
    total_files = len(results)

    # Summary panel
    summary_text = Text()
    summary_text.append(f"{total_files} file(s) analyzed", style="bold white")
    if total_errors:
        summary_text.append(f"  |  {total_errors} errors", style="bold red")
    if total_warnings:
        summary_text.append(f"  |  {total_warnings} warnings", style="bold yellow")
    if total_info:
        summary_text.append(f"  |  {total_info} info", style="dim cyan")
    if total_errors == 0 and total_warnings == 0:
        summary_text.append("  |  ✨ All clear!", style="bold green")

    console.print(Panel(summary_text, title="pyreview", border_style="blue"))
    console.print()

    for result in results:
        if not result.issues:
            continue

        table = Table(
            title=f"{result.file}",
            box=box.ROUNDED,
            border_style="dim blue",
            show_header=True,
            header_style="bold",
        )
        table.add_column("Line", style="dim cyan", width=6)
        table.add_column("Severity", width=10)
        table.add_column("Check", style="dim", width=16)
        table.add_column("Issue", ratio=1)

        for issue in sorted(result.issues, key=lambda i: i.line):
            sev_style = SEVERITY_COLORS.get(issue.severity, "white")
            icon = SEVERITY_ICONS.get(issue.severity, "")
            table.add_row(
                str(issue.line),
                f"{icon} {issue.severity}",
                issue.check,
                issue.message,
                style=sev_style,
            )

            if issue.suggestion:
                table.add_row(
                    "",
                    "",
                    "",
                    f"  💡 {issue.suggestion}",
                    style="dim italic",
                )

        console.print(table)
        console.print()

def _format_json(results: list[AnalysisResult]) -> None:
    output = []
    for result in results:
        output.append({
            "file": result.file,
            "errors": result.error_count,
            "warnings": result.warning_count,
            "info": result.info_count,
            "issues": [
                {
                    "line": i.line,
                    "check": i.check,
                    "severity": i.severity,
                    "message": i.message,
                    "suggestion": i.suggestion,
                }
                for i in result.issues
            ],
        })
    console.print_json(data=output)

def _format_summary(results: list[AnalysisResult]) -> None:
    total_errors = sum(r.error_count for r in results)
    total_warnings = sum(r.warning_count for r in results)

    for result in results:
        icon = "✅" if result.error_count == 0 else "❌"
        console.print(
            f"  {icon} {result.file}  "
            f"[red]{result.error_count} errors[/red]  "
            f"[yellow]{result.warning_count} warnings[/yellow]"
        )

    console.print()
    if total_errors == 0 and total_warnings == 0:
        console.print("[bold green]All checks passed! No issues found.[/bold green]")
    else:
        console.print(f"[bold]Total: [red]{total_errors} errors[/red], [yellow]{total_warnings} warnings[/yellow][/bold]")

Step 9: Running the Tool

After implementation, run it against your own code or the test fixtures:

bash
# Install in development mode
pip install -e .

# Run against a single file
pyreview my_module.py

# Run against a directory
pyreview ./src --severity info

# JSON output for CI pipelines
pyreview ./src --format json

# Only specific checks
pyreview ./src --checks encapsulation inheritance

# Summary mode
pyreview ./src --format summary

Step 10: Testing

Write tests against the included fixtures. Create tests/conftest.py:

python
"""Shared test fixtures."""
from pathlib import Path

FIXTURES_DIR = Path(__file__).parent / "fixtures"

def load_fixture(name: str) -> str:
    return (FIXTURES_DIR / name).read_text()

Create a fixture file tests/fixtures/bad_encapsulation.py:

python
class BadEncapsulation:
    def __init__(self):
        self.name = "test"        # public, mutable
        self.age = 25              # public, mutable
        self.email = "[email protected]"    # public, mutable
        self.address = "123 St"    # public, mutable
        self.phone = "555-0000"    # public, mutable
        self.active = True         # public, mutable
        self.role = "admin"        # public, mutable  (7 total = triggers warning)

Then write tests/test_encapsulation.py:

python
"""Tests for encapsulation checks."""
import ast
from pyreview.checks.encapsulation import check_encapsulation

def test_detects_too_many_public_attributes():
    source = """
class BadEncapsulation:
    def __init__(self):
        self.name = "test"
        self.age = 25
        self.email = "[email protected]"
        self.address = "123 St"
        self.phone = "555-0000"
        self.active = True
        self.role = "admin"
"""
    tree = ast.parse(source)
    issues = check_encapsulation(tree, "test.py")
    assert len(issues) >= 1
    assert any("public attributes" in i.message for i in issues)

def test_clean_class_passes():
    source = """
class CleanClass:
    def __init__(self, name: str):
        self._name = name

    @property
    def name(self) -> str:
        return self._name
"""
    tree = ast.parse(source)
    issues = check_encapsulation(tree, "test.py")
    assert len(issues) == 0

What You'll Have Built

By the end of this project, you'll have a fully functional CLI tool that:

  • Scans Python files and directories
  • Detects 10+ OOP anti-patterns across 4 categories
  • Produces beautiful terminal output with rich
  • Exports JSON for CI/CD integration
  • Has a test suite with fixture-based testing
  • Uses proper Python packaging with pyproject.toml

Extension Ideas

  • Add a --fix mode that auto-generates refactoring suggestions
  • Integrate with pre-commit hooks for automated PR checks
  • Add more checks: LSP violations, circular dependencies, interface segregation
  • Generate a compliance score (0-100) per file
  • Add GitHub Actions integration to run pyreview on every PR

Requirements

Prerequisites

  • Python 3.10+
  • Familiarity with OOP concepts: classes, inheritance, encapsulation, polymorphism
  • Basic CLI/terminal usage
  • Git (for version control)

Starter Files

pyproject.tomltoml
pyreview/__init__.pypython
pyreview/__main__.pypython
pyreview/cli.pypython
pyreview/analyzer.pypython
pyreview/checks/__init__.pypython
pyreview/checks/encapsulation.pypython
pyreview/checks/inheritance.pypython
pyreview/checks/cohesion.pypython
pyreview/checks/naming.pypython
pyreview/reporter.pypython

Workspace

Project Sandbox
1[build-system]2requires = ["setuptools>=68.0", "wheel"]3build-backend = "setuptools.build_meta"4 5[project]6name = "pyreview"7version = "0.1.0"8description = "CLI tool to audit Python OOP code for best practices"9requires-python = ">=3.10"10dependencies = [11    "rich>=13.0",12]13optional-dependencies.test = [14    "pytest>=7.0",15]16 17[project.scripts]18pyreview = "pyreview.cli:main"19 
Click inside code viewer to edit
Edits are preserved locally. This file type does not support in-browser runtime execution.
Sign in to submit your project for review.