Skip to content

Commit

Permalink
Merge pull request #69 from gauge-sh/exact-config
Browse files Browse the repository at this point in the history
strict -> exact, configurable in tach.yml, docs updates, fix argv[0] assumption
  • Loading branch information
caelean authored May 23, 2024
2 parents 69a5a4b + 8994835 commit 9bd0f2d
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 139 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,4 @@ cython_debug/

.python-version
.env.leave
.DS_Store
163 changes: 54 additions & 109 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,154 +4,99 @@
[![image](https://github.com/gauge-sh/tach/actions/workflows/ci.yml/badge.svg)](https://github.com/gauge-sh/tach/actions/workflows/ci.yml)
[![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
# tach
# Tach
a Python tool to enforce modular design


[Docs](https://gauge-sh.github.io/tach/)

[Discord](https://discord.gg/DKVksRtuqS) - come say hi!

Tach lets you define and enforce dependencies across Python packages in your project. A Python package is any directory that contains an `__init__.py`.

This enforces a decoupled, modular architecture, which makes maintenance and development easier. If a package tries to import from another package that is not listed as a dependency, `tach` will throw an exception.

https://github.com/gauge-sh/tach/assets/10570340/2f5ed866-124e-4322-afe6-15207727ca38

Here's an example:

![tach_demo_ds](https://github.com/gauge-sh/tach/assets/5150563/c693da70-6f5d-417c-968e-4d0507d957c0)

## What is tach?
`tach` allows you to define boundaries and control dependencies between your Python packages. Each package can also define its public interface.

This enforces a decoupled, modular architecture, and prevents tight coupling.
If a package tries to import from another package that is not listed as a dependency, tach will report an error.
If a package tries to import from another package and does not use its public interface, with `strict: true` set, `tach` will report an error.
Tach is:
- 🌎 Open source
- 🐍 Installable via pip
- 🔧 Able to be adopted incrementally
- ⚡ Implemented with no runtime impact
- ♾️ Interoperable with your existing systems (cli, hooks, ci, etc.)

`tach` is incredibly lightweight, and has no impact on your runtime. Instead, its checks are performed as a lint check through the CLI.
## Getting Started

## Installation
### Installation
```bash
pip install tach
```
### Setup
Tach allows you to configure what is and is not considered a package. By default, Tach will identify and create configuration for all top level packages it finds.

## Quickstart
`tach` comes bundled with a command to interactively define your package boundaries.
Run the following in the root of your Python project to enter the editor:
You can do this interactively! From the root of your python project, run:
```bash
tach pkg
tach pkg
# Up/Down: Navigate Ctrl + Up: Jump to parent Right: Expand Left: Collapse
# Ctrl + c: Exit without saving Ctrl + s: Save packages Enter: Mark/unmark package Ctrl + a: Mark/unmark all siblings
```
Mark and unmark each package as needed, depending on what you want to define boundaries for.

The interactive editor allows you to mark which directories should be treated as package boundaries.
You can navigate with the arrow keys, mark individual packages with `Enter`, and mark all sibling directories
as packages with `Ctrl + a`.

After identifying your packages, press `Ctrl + s` to initialize the boundaries.
Each package will receive a `package.yml` with a single tag based on the folder name,
and a default `tach.yml` file will be created in the current working directory.

If you want to sync your `tach.yml` with the actual dependencies found in your project, you can use `tach sync`:
Once you have marked all the packages you want to enforce constraints between, run:
```bash
tach sync [--prune]
tach sync
```
This will create the root configuration for your project, `tach.yml`, with the dependencies that currently exist between each package you've marked.

Any dependency errors will be automatically resolved by
adding the corresponding dependencies to your `tach.yml` file. If you supply `--prune`,
any dependency constraints in your `tach.yml` which are not necessary will also be removed.

In case you want to start over, `tach clean` lets you delete all `tach` configuration files so that you can re-initialize or configure your packages manually.
```bash
tach clean
You can then see what Tach has found by viewing the `tach.yml`'s contents:
```
cat tach.yml
```

Note: Dependencies on code that are not marked as packages are out of the scope of Tach and will not be enforced.

## Defining Packages
To define a package, add a `package.yml` to the corresponding Python package. Add at least one 'tag' to identify the package.

Examples:
```python
# core/package.yml
tags: ["core"]
```
```python
# db/package.yml
tags: ["db"]
```
```python
# utils/package.yml
tags: ["utils"]
```
Next, specify the constraints for each tag in `tach.yml` in the root of your project:
```yaml
# [root]/tach.yml
constraints:
- tag: core
depends_on:
- db
- utils
- tag: db
depends_on:
- utils
- tag: utils
depends_on: []
### Enforcement
Tach comes with a simple cli command to enforce the boundaries that you just set up! From the root of your Python project, run:
```bash
tach check
```
With these rules in place, packages with tag `core` can import from packages with tag `db` or `utils`. Packages tagged with `db` can only import from `utils`, and packages tagged with `utils` cannot import from any other packages in the project.

`tach` will now flag any violation of these boundaries.
You will see:
```bash
# From the root of your Python project (in this example, `project/`)
> tach check
❌ utils/helpers.py[L10]: Cannot import 'core.PublicAPI'. Tags ['utils'] cannot depend on ['core'].
✅ All package dependencies validated!
```

NOTE: If your terminal supports hyperlinks, you can click on the failing file path to go directly to the error.
## Defining Interfaces
If you want to define a public interface for the package, import and reference each object you want exposed in the package's `__init__.py` and add its name to `__all__`:
```python
# db/__init__.py
from db.service import PublicAPI
You can validate that Tach is working by either commenting out an item in a `depends_on` key in `tach.yml`, or by adding an import between packages that didn't previously import from each other.

__all__ = ["PublicAPI"]
```
Turning on `strict: true` in the package's `package.yml` will then enforce that all imports from this package occur through `__init__.py` and are listed in `__all__`
```yaml
# db/package.yml
tags: ["db"]
strict: true
```
```python
# The only valid import from "db"
from db import PublicAPI
Give both a try and run `tach check` again. This will generate an error:
```bash
❌ path/file.py[LNO]: Cannot import 'path.other'. Tags ['scope:other'] cannot depend on ['scope:file'].
```

### Pre-Commit Hook
`tach` can be installed as a pre-commit hook. See the [docs](https://gauge-sh.github.io/tach/usage/#tach-install) for installation instructions.
### Extras


## Advanced
`tach` supports specific exceptions. You can mark an import with the `tach-ignore` comment:
```python
# tach-ignore
from db.main import PrivateAPI
If an error is generated that is an intended dependency, you can sync your actual dependencies with `tach.yml`:
```bash
tach sync
```
This will stop `tach` from flagging this import as a boundary violation.
After running this command, `tach check` will always pass.

You can also specify multiple tags for a given package:
```python
# utils/package.yml
tags: ["core", "utils"]
If your configuration is in a bad state, from the root of your python project you can run:
```bash
tach clean
```
This will expand the set of packages that "utils" can access to include all packages that "core" and "utils" `depends_on` as defined in `tach.yml`.
This will wipe all the configuration generated and enforced by Tach.

By default, `tach` ignores hidden directories and files (paths starting with `.`). To override this behavior, set `exclude_hidden_paths` in `tach.yml`
```yaml
exclude_hidden_paths: false
```

## Details
`tach` works by analyzing the abstract syntax tree (AST) of your codebase. It has no runtime impact, and all operations are performed statically.
Tach also supports:
- [Manual file configuration](https://gauge-sh.github.io/tach/configuration/)
- [Strict public interfaces for packages](https://gauge-sh.github.io/tach/strict-mode/)
- [Inline exceptions](https://gauge-sh.github.io/tach/tach-ignore/)
- [Pre-commit hooks](https://gauge-sh.github.io/tach/usage/#tach-install)

Boundary violations are detected at the import layer. This means that dynamic imports using `importlib` or similar approaches will not be caught by tach.

[PyPi Package](https://pypi.org/project/tach/)
More info in the [docs](https://gauge-sh.github.io/tach/).
If you have any feedback, we'd love to hear it!

### License
[GNU GPLv3](LICENSE)
[Discord](https://discord.gg/a58vW8dnmw)
64 changes: 48 additions & 16 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,65 @@
# Getting Started
### Installation
```bash
pip install tach
```
### Setup
Tach allows you to configure what is and is not considered a package. By default, Tach will identify and create configuration for all top level packages it finds.

## Installation
You can do this interactively! From the root of your python project, run:
```bash
tach pkg
# Up/Down: Navigate Ctrl + Up: Jump to parent Right: Expand Left: Collapse
# Ctrl + c: Exit without saving Ctrl + s: Save packages Enter: Mark/unmark package Ctrl + a: Mark/unmark all siblings
```
Mark and unmark each package as needed, depending on what you want to define boundaries for.

[PyPi package](https://pypi.org/project/tach/)
Once you have marked all the packages you want to enforce constraints between, run:
```bash
tach sync
```
This will create the root configuration for your project, `tach.yml`, with the dependencies that currently exist between each package you've marked.

You can then see what Tach has found by viewing the `tach.yml`'s contents:
```
cat tach.yml
```

Install tach into a Python environment with `pip`
Note: Dependencies on code that are not marked as packages are out of the scope of Tach and will not be enforced.

### Enforcement
Tach comes with a simple cli command to enforce the boundaries that you just set up! From the root of your Python project, run:
```bash
pip install tach
tach check
```

Verify your installation is working correctly
You will see:
```bash
tach -h
✅ All package dependencies validated!
```

## Adding to a Project
You can validate that Tach is working by either commenting out an item in a `depends_on` key in `tach.yml`, or by adding an import between packages that didn't previously import from each other.

If you are adding `tach` to an existing project, you have two main options:
Give both a try and run `tach check` again. This will generate an error:
```bash
❌ path/file.py[LNO]: Cannot import 'path.other'. Tags ['scope:other'] cannot depend on ['scope:file'].
```

1. Use [`tach pkg`](usage.md#tach-pkg) to interactively set up packages, and [`tach sync`](usage.md#tach-sync) to automatically set up dependency rules.
2. Manually configure your [packages](configuration.md#packageyml) and [dependency rules](configuration.md#tachyml)
### Extras

## Checking Boundaries
If an error is generated that is an intended dependency, you can sync your actual dependencies with `tach.yml`:
```bash
tach sync
```
After running this command, `tach check` will always pass.

If your configuration is in a bad state, from the root of your python project you can run:
```bash
# From the root of your Python project
tach check
tach clean
```
This will wipe all the configuration generated and enforced by Tach.


After guarding your project, running `tach check` from the root will check all imports to verify that packages remain correctly decoupled.
Tach also supports:
- [Manual file configuration](https://gauge-sh.github.io/tach/configuration/)
- [Strict public interfaces for packages](https://gauge-sh.github.io/tach/strict-mode/)
- [Inline exceptions](https://gauge-sh.github.io/tach/tach-ignore/)
- [Pre-commit hooks](https://gauge-sh.github.io/tach/usage/#tach-install)
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ When a package is in ['strict mode'](strict-mode.md), if another package tries t

`tach` runs on the CLI, and is ideal for pre-commit hooks and CI checks.

Tach is:
- 🌎 Open source
- 🐍 Installable via pip
- 🔧 Able to be adopted incrementally
- ⚡ Implemented with no runtime impact
- ♾️ Interoperable with your existing systems (cli, hooks, ci, etc.)

## Commands
* [`tach pkg`](usage.md#tach-pkg) - Interactively define package boundaries in your Python project.
* [`tach check`](usage.md#tach-check) - Check that boundaries are respected.
Expand Down
Binary file removed docs/tach_demo.mp4
Binary file not shown.
11 changes: 7 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
`tach` will flag any unwanted imports between packages. We recommend you run `tach check` like a linter or test runner, e.g. in pre-commit hooks, on-save hooks, and in CI pipelines.

```bash
usage: tach check [-h] [-e file_or_path,...]
usage: tach check [-h] [--exact] [-e file_or_path,...]

Check boundaries with tach
Check existing boundaries against your dependencies and package interfaces

options:
-h, --help show this help message and exit
--exact Raise errors if any dependency constraints are unused.
-e file_or_path,..., --exclude file_or_path,...
Comma separated path list to exclude. tests/, ci/, etc.
```
Expand All @@ -20,6 +21,8 @@ An error will indicate:
- the tags associated with that file
- the tags associated with the attempted import
If `--exact` is provided, additional errors will be raised if a dependency exists in `tach.yml` that is not exercised by the code.
Example:
```bash
# From the root of your Python project (in this example, `project/`)
Expand Down Expand Up @@ -56,7 +59,7 @@ The `--depth` flag controls how many directories `tach` will traverse when sugge
You can accept these suggestions immediately with `Ctrl + s`, or you can edit the selections freely before confirming.
Any time you make changes with `tach pkg`, it is recommended to run [`tach sync`](usage.md#tach-sync)
to automatically set up dependency rules.
to automatically configure dependency rules.
## tach sync
`tach` can automatically sync your project configuration (`tach.yml`) with your project's actual dependencies.
Expand Down Expand Up @@ -107,7 +110,7 @@ If you use the [pre-commit framework](https://github.com/pre-commit/pre-commit),
```yaml
repos:
- repo: https://github.com/gauge-sh/tach
rev: v0.2.3 # change this to the latest tag!
rev: v0.2.4 # change this to the latest tag!
hooks:
- id: tach
# args: ["--root=backend_root"]
Expand Down
19 changes: 19 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ theme:
- navigation.sections
- navigation.tracking
- toc.follow
palette:
# Palette toggle for automatic mode
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to system preference

markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "tach"
version = "0.2.3"
version = "0.2.4"
authors = [
{ name="Caelean Barnes", email="[email protected]" },
{ name="Evan Doyle", email="[email protected]" },
Expand All @@ -16,6 +16,7 @@ classifiers = [
"Environment :: Console",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down
Loading

0 comments on commit 9bd0f2d

Please sign in to comment.