Skip to content

Implement usethis tool tach for architecture enforcement#1639

Merged
nathanjmcdougall merged 17 commits intomainfrom
copilot/implement-usethis-tool-tach
Apr 1, 2026
Merged

Implement usethis tool tach for architecture enforcement#1639
nathanjmcdougall merged 17 commits intomainfrom
copilot/implement-usethis-tool-tach

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 29, 2026

  • Create src/usethis/_tool/impl/spec/tach.py - Tach tool specification (config, deps, pre-commit)
  • Create src/usethis/_tool/impl/base/tach.py - Tach tool implementation
  • Add TachTOMLManager to src/usethis/_config_file.py for tach.toml file management
  • Register TachTool in src/usethis/_tool/all_.py
  • Add use_tach() function to src/usethis/_core/tool.py
  • Add tach CLI command to src/usethis/_ui/interface/tool.py
  • Create tests: tests/usethis/_tool/impl/base/test_tach.py (100% coverage on spec+base)
  • Add tests for use_tach() in tests/usethis/_core/test_core_tool.py (add, config, pre-commit, remove, how)
  • Add CLI tests for usethis tool tach in tests/usethis/_ui/interface/test_tool.py (add, how, remove)
  • Update .importlinter contracts to include tach modules
  • Update AGENTS.md and docs/module-tree.txt sync
  • Fix missing docstrings for use_tach and tach CLI functions
  • Update docs/functions.txt and AGENTS.md sync blocks
  • Revert _toolset/arch.py — tach is an alternative to import-linter, not a complement
  • Register "tach" in _HOOK_ORDER in _integrations/pre_commit/hooks.py
  • Add TachTool to OTHER_TOOLS in pyproject_toml.py and update usage table test expectations
  • Update tach CLI help text to "enforce self-imposed dependency and interface rules"
  • Fix tach spec to only generate root-level modules — nested sub-module architectures cannot be expressed in tach's flat module format (unlike import-linter's scoped containers), so only min_depth entries are included
  • Dogfood usethis tool tach against the usethis project — verified tach check passes
  • User testing in a fresh temporary project — verified add, how, remove, pre-commit integration, and tach check all work correctly on a clean project
  • Add tach to docs: README.md, docs/cli/overview.md, and docs/cli/reference.md
  • Simplify tach.toml output: remove verbose depends_on fields (tach infers from layer ordering), use module paths as layer names instead of generic "layer1", "layer2"
  • Clarify min_depth comment in tach spec to explain relationship with TACH_CONTRACT_MIN_MODULE_COUNT
  • Run static checks (prek ✅, basedpyright ✅)

Resolves #1254


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI linked an issue Mar 29, 2026 that may be closed by this pull request
Copilot AI and others added 2 commits March 29, 2026 23:22
Add Tach tool integration that:
- Creates tach.toml configuration with layers inferred from import graph
- Adds tach as a dev dependency
- Configures pre-commit hook for tach check
- Registers tach in the arch toolset alongside Import Linter
- Adds CLI command: usethis tool tach

Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/e208815b-6629-4e78-aaad-bc82cbedace0

Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
…or tach

Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/e208815b-6629-4e78-aaad-bc82cbedace0

Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement usethis tool tach for layered architecture Implement usethis tool tach for architecture enforcement Mar 29, 2026
Copilot AI requested a review from nathanjmcdougall March 29, 2026 23:30
@nathanjmcdougall nathanjmcdougall marked this pull request as ready for review March 30, 2026 00:00
@nathanjmcdougall
Copy link
Copy Markdown
Collaborator

@copilot failing static checks, please fix

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot failing static checks, please fix

Fixed in 19c91fb — the use_tach and tach CLI functions were missing docstrings required by the export-functions hook (which runs with --strict). Also updated docs/functions.txt and the AGENTS.md sync block.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 30, 2026

Merging this PR will degrade performance by 5.95%

❌ 1 regressed benchmark
✅ 1 untouched benchmark

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation test_help_flag 40.5 ms 43 ms -5.95%

Comparing copilot/implement-usethis-tool-tach (03db33a) with main (3cf5534)

Open in CodSpeed

Comment thread src/usethis/_toolset/arch.py Outdated
Comment thread src/usethis/_core/tool.py
…d use_tach tests

Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/aa9351bc-3fdf-4933-8e5d-48f36e6c7245

Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
@nathanjmcdougall
Copy link
Copy Markdown
Collaborator

@copilot tests are failing, please fix.

Details

=================================== FAILURES ===================================
_____________________ TestGetUsageTable.test_empty_project _____________________

self = <tests.usethis._core.test_list.TestGetUsageTable object at 0x7fc5307e2980>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-0/test_empty_project1')

def test_empty_project(self, tmp_path: Path):
    # Act
    with change_cwd(tmp_path), files_manager():
        table = get_usage_table()

    # Assert
    expected = UsageTable(
        rows=[
            UsageRow(category="tool", name="Codespell", status="unused"),
            UsageRow(category="tool", name="Coverage.py", status="unused"),
            UsageRow(category="tool", name="deptry", status="unused"),
            UsageRow(category="tool", name="Import Linter", status="unused"),
            UsageRow(category="tool", name="MkDocs", status="unused"),
            UsageRow(category="tool", name="pre-commit", status="unused"),
            UsageRow(category="tool", name="pyproject-fmt", status="unused"),
            UsageRow(category="tool", name="pyproject.toml", status="unused"),
            UsageRow(category="tool", name="pytest", status="unused"),
            UsageRow(category="tool", name="requirements.txt", status="unused"),
            UsageRow(category="tool", name="Ruff", status="unused"),
            UsageRow(category="tool", name="ty", status="unused"),
            UsageRow(category="config", name="docstyle", status="unused"),
            UsageRow(category="", name="README", status="unused"),
        ],
    )
  assert table == expected

E AssertionError: assert UsageTable(ro...us='unused')]) == UsageTable(ro...us='unused')])
E
E Full diff:
E - UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
E ? ^^^^^^^^^^^^^^^^^^^^^^
E + UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='Tach', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
E ? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

expected = UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverag... UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
self = <tests.usethis._core.test_list.TestGetUsageTable object at 0x7fc5307e2980>
table = UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverag... UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-0/test_empty_project1')

tests/usethis/_core/test_list.py:51: AssertionError
_______________________ TestGetUsageTable.test_ruff_used _______________________

self = <tests.usethis._core.test_list.TestGetUsageTable object at 0x7fc5307e0d00>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-0/test_ruff_used0')

def test_ruff_used(self, tmp_path: Path):
    # Arrange
    (tmp_path / "ruff.toml").touch()

    # Act
    with change_cwd(tmp_path), files_manager():
        table = get_usage_table()

    # Assert
    expected = UsageTable(
        rows=[
            UsageRow(category="tool", name="Codespell", status="unused"),
            UsageRow(category="tool", name="Coverage.py", status="unused"),
            UsageRow(category="tool", name="deptry", status="unused"),
            UsageRow(category="tool", name="Import Linter", status="unused"),
            UsageRow(category="tool", name="MkDocs", status="unused"),
            UsageRow(category="tool", name="pre-commit", status="unused"),
            UsageRow(category="tool", name="pyproject-fmt", status="unused"),
            UsageRow(category="tool", name="pyproject.toml", status="unused"),
            UsageRow(category="tool", name="pytest", status="unused"),
            UsageRow(category="tool", name="requirements.txt", status="unused"),
            UsageRow(category="tool", name="Ruff", status="used"),
            UsageRow(category="tool", name="ty", status="unused"),
            UsageRow(category="config", name="docstyle", status="unused"),
            UsageRow(category="", name="README", status="unused"),
        ],
    )
  assert table == expected

E AssertionError: assert UsageTable(ro...us='unused')]) == UsageTable(ro...us='unused')])
E
E Full diff:
E - UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='used'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
E ? ^^^^^^^^^^^^^^^^^^^^^^
E + UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='used'), UsageRow(category='tool', name='Tach', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
E ? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

expected = UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverag... UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
self = <tests.usethis._core.test_list.TestGetUsageTable object at 0x7fc5307e0d00>
table = UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverag... UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-0/test_ruff_used0')

tests/usethis/_core/test_list.py:80: AssertionError
_____________________ TestGetUsageTable.test_uv_init_case ______________________

self = <tests.usethis._core.test_list.TestGetUsageTable object at 0x7fc5307e33a0>
uv_init_dir = PosixPath('/tmp/pytest-of-runner/pytest-0/test_uv_init_case0')

def test_uv_init_case(self, uv_init_dir: Path):
    # Act
    with change_cwd(uv_init_dir), files_manager():
        table = get_usage_table()

    # Assert
    expected = UsageTable(
        rows=[
            UsageRow(category="tool", name="Codespell", status="unused"),
            UsageRow(category="tool", name="Coverage.py", status="unused"),
            UsageRow(category="tool", name="deptry", status="unused"),
            UsageRow(category="tool", name="Import Linter", status="unused"),
            UsageRow(category="tool", name="MkDocs", status="unused"),
            UsageRow(category="tool", name="pre-commit", status="unused"),
            UsageRow(category="tool", name="pyproject-fmt", status="unused"),
            UsageRow(category="tool", name="pyproject.toml", status="used"),
            UsageRow(category="tool", name="pytest", status="unused"),
            UsageRow(category="tool", name="requirements.txt", status="unused"),
            UsageRow(category="tool", name="Ruff", status="unused"),
            UsageRow(category="tool", name="ty", status="unused"),
            UsageRow(category="config", name="docstyle", status="unused"),
            UsageRow(category="", name="README", status="used"),
        ],
    )
  assert table == expected

E AssertionError: assert UsageTable(ro...atus='used')]) == UsageTable(ro...atus='used')])
E
E Full diff:
E - UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='used'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='used')])
E ? ^^^^^^^^^^^^^^^^^^^^^^
E + UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='used'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='Tach', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='used')])
E ? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

expected = UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverag...), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='used')])
self = <tests.usethis._core.test_list.TestGetUsageTable object at 0x7fc5307e33a0>
table = UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverag...), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='used')])
uv_init_dir = PosixPath('/tmp/pytest-of-runner/pytest-0/test_uv_init_case0')

/home/runner/work/usethis-python/usethis-python/tests/usethis/_core/test_list.py:106: AssertionError
__________________ TestOtherTools.test_in_sync_with_all_tools __________________

self = <tests.usethis._tool.impl.base.test_pyproject_toml.TestOtherTools object at 0x7fc52fabdfc0>

def test_in_sync_with_all_tools(self):
  assert {tool.name.lower() for tool in OTHER_TOOLS} | {
        PyprojectTOMLTool().name
    } == {tool.name.lower() for tool in ALL_TOOLS}

E AssertionError: assert {'codespell',...-commit', ...} == {'codespell',...-commit', ...}
E
E Extra items in the right set:
E 'tach'
E
E Full diff:
E {
E 'codespell',
E 'coverage.py',
E 'deptry',
E 'import linter',
E 'mkdocs',
E 'pre-commit',
E 'pyproject-fmt',
E 'pyproject.toml',
E 'pytest',
E 'requirements.txt',
E 'ruff',
E - 'tach',
E 'ty',
E }

self = <tests.usethis._tool.impl.base.test_pyproject_toml.TestOtherTools object at 0x7fc52fabdfc0>

tests/usethis/_tool/impl/base/test_pyproject_toml.py:15: AssertionError
---------------- generated Markdown report: /tmp/tmp.jqbeCrH6qE ----------------
=========================== short test summary info ============================
SKIPPED [14] tests/usethis/test_fallback.py:56: Avoid flaky pipelines by testing version bumps manually
SKIPPED [1] tests/usethis/_tool/test_base.py:631: Multiple hooks in one repo not supported yet.
SKIPPED [1] tests/usethis/_tool/test_base.py:771: Multiple hooks in one repo not supported yet
SKIPPED [1] tests/usethis/_tool/test_base.py:808: Multiple hooks in one repo not supported yet
FAILED tests/usethis/_core/test_list.py::TestGetUsageTable::test_empty_project - AssertionError: assert UsageTable(ro...us='unused')]) == UsageTable(ro...us='unused')])

Full diff:

  • UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
    ? ^^^^^^^^^^^^^^^^^^^^^^
  • UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='Tach', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
    ? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    FAILED tests/usethis/_core/test_list.py::TestGetUsageTable::test_ruff_used - AssertionError: assert UsageTable(ro...us='unused')]) == UsageTable(ro...us='unused')])

Full diff:

  • UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='used'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
    ? ^^^^^^^^^^^^^^^^^^^^^^
  • UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='unused'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='used'), UsageRow(category='tool', name='Tach', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='unused')])
    ? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    FAILED tests/usethis/_core/test_list.py::TestGetUsageTable::test_uv_init_case - AssertionError: assert UsageTable(ro...atus='used')]) == UsageTable(ro...atus='used')])

Full diff:

  • UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='used'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='used')])
    ? ^^^^^^^^^^^^^^^^^^^^^^
  • UsageTable(rows=[UsageRow(category='tool', name='Codespell', status='unused'), UsageRow(category='tool', name='Coverage.py', status='unused'), UsageRow(category='tool', name='deptry', status='unused'), UsageRow(category='tool', name='Import Linter', status='unused'), UsageRow(category='tool', name='MkDocs', status='unused'), UsageRow(category='tool', name='pre-commit', status='unused'), UsageRow(category='tool', name='pyproject-fmt', status='unused'), UsageRow(category='tool', name='pyproject.toml', status='used'), UsageRow(category='tool', name='pytest', status='unused'), UsageRow(category='tool', name='requirements.txt', status='unused'), UsageRow(category='tool', name='Ruff', status='unused'), UsageRow(category='tool', name='Tach', status='unused'), UsageRow(category='tool', name='ty', status='unused'), UsageRow(category='config', name='docstyle', status='unused'), UsageRow(category='', name='README', status='used')])
    ? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    FAILED tests/usethis/_tool/impl/base/test_pyproject_toml.py::TestOtherTools::test_in_sync_with_all_tools - AssertionError: assert {'codespell',...-commit', ...} == {'codespell',...-commit', ...}

Extra items in the right set:
'tach'

Full diff:
{
'codespell',
'coverage.py',
'deptry',
'import linter',
'mkdocs',
'pre-commit',
'pyproject-fmt',
'pyproject.toml',
'pytest',
'requirements.txt',
'ruff',

  • 'tach',
    'ty',
    
    }
    ============ 4 failed, 1520 passed, 17 skipped in 131.12s (0:02:11) ============
    Error: Process completed with exit code 1.
    Run echo "::group::..."
    ...

=====================================================================================
Markdown summaries: https://github.com/usethis-python/usethis-python/actions/runs/23722634434

Comment thread src/usethis/_ui/interface/tool.py Outdated
@nathanjmcdougall
Copy link
Copy Markdown
Collaborator

@copilot please apply user testing for the CLI changes

@nathanjmcdougall
Copy link
Copy Markdown
Collaborator

@copilot we need to add the new command to the docs in README.md among other places.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot please apply user testing for the CLI changes

User testing completed successfully in a fresh temporary project. Tested all three CLI paths:

  1. usethis tool tach — adds tach dependency, creates tach.toml with correct source roots/exclusions, and generates valid layer config from actual modules. tach check passes.
  2. usethis tool tach --how — outputs the expected instruction message.
  3. usethis tool tach --remove — cleanly removes tach.toml and the dependency from pyproject.toml.
  4. With pre-commit already set up, usethis tool tach correctly adds the tach hook to .pre-commit-config.yaml.

No issues discovered — all paths work as expected on a fresh project.

…ence.md

Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/0f53d301-459d-43cf-b4c2-97a82bfddabe

Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot we need to add the new command to the docs in README.md among other places.

Added in 8ef0448usethis tool tach is now documented in README.md, docs/cli/overview.md, and docs/cli/reference.md under Code Quality Tools, following the same pattern as the other tools.

@nathanjmcdougall
Copy link
Copy Markdown
Collaborator

@copilot the tach.toml contents are quite versbose by including the depends_on field. This isn't necessary and just makes things verbose.

Instead of naming the layers "layer1", "layer2" etc. let's just name them the same as the module path.

…aths as layer names

Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/78ef23db-f7f0-4b8b-8207-ba0b8d529c8d

Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot the tach.toml contents are quite versbose by including the depends_on field. This isn't necessary and just makes things verbose.

Instead of naming the layers "layer1", "layer2" etc. le...

Fixed in e44af81 — removed depends_on from all module entries and switched layer names to use the full module path (e.g. usethis._core) instead of generic layer0, layer1. Tach infers allowed dependencies from the layer ordering, so depends_on is redundant. Verified tach check still passes on the usethis project via dogfooding.

Comment thread src/usethis/_tool/impl/spec/tach.py Outdated
…MIN_MODULE_COUNT relationship

Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/0e90e864-519d-487d-a92b-c288a19e2112

Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
@nathanjmcdougall nathanjmcdougall merged commit 78b7bd4 into main Apr 1, 2026
19 of 20 checks passed
@nathanjmcdougall nathanjmcdougall deleted the copilot/implement-usethis-tool-tach branch April 1, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement usethis tool tach

2 participants