Skip to content

Commit

Permalink
Merge pull request #428 from gauge-sh/rebrand-strict-mode
Browse files Browse the repository at this point in the history
[0.15.0] Move from 'strict mode' to explicit interfaces
  • Loading branch information
emdoyle authored Nov 21, 2024
2 parents c650c6d + c77022d commit 689a505
Show file tree
Hide file tree
Showing 27 changed files with 527 additions and 189 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tach"
version = "0.14.4"
version = "0.15.0"
edition = "2021"

[lib]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ python/tach/cache/setup.py[L7]: Import 'tach.filesystem.find_project_config_root

Tach also supports:

- [Strict public interfaces for modules](https://docs.gauge.sh/usage/strict-mode/)
- [Public interfaces for modules](https://docs.gauge.sh/usage/interfaces/)
- [Deprecating individual dependencies](https://docs.gauge.sh/usage/deprecate)
- [Incremental adoption](https://docs.gauge.sh/usage/unchecked-modules)
- [Manual file configuration](https://docs.gauge.sh/usage/configuration)
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Give both a try and run `tach check` again. This will generate an error:

Each error indicates an import which violates your dependencies. If your terminal supports hyperlinks, click on the file path to go directly to the error.

`tach check` will also return an error code. It can be easily integrated with CI/CD, [Pre-commit hooks](../usage/commands#tach-install), and [VS Code](https://marketplace.visualstudio.com/items?itemName=Gauge.tach), and more!
Tach can easily be added to your CI pipeline, [pre-commit hooks](../usage/commands#tach-install), and [VS Code](https://marketplace.visualstudio.com/items?itemName=Gauge.tach).

### Extras

Expand Down Expand Up @@ -111,7 +111,7 @@ python/tach/cache/setup.py[L7]: Import 'tach.filesystem.find_project_config_root

Tach also supports:

- [Strict public interfaces for modules](../usage/strict-mode)
- [Public interfaces for modules](../usage/interfaces)
- [Deprecating individual dependencies](../usage/deprecate)
- [Manual file configuration](../usage/configuration)
- [Monorepos and namespace packages](../usage/configuration#source-roots)
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This creates a modular architecture, which makes development easier.

If a module tries to import from another module that is not listed as a dependency, Tach will report an error.

When a module is marked ['strict'](../usage/strict-mode), if another module tries to import from it without using its public interface, Tach will report an error.
When a module has a [public interface](../usage/interfaces), any import which does not go through the public interface will cause Tach to report an error.

Dependencies can be additionally marked as ['deprecated'](../usage/deprecate). Tach will not report an error but will surface usages of the deprecated dependency.

Expand All @@ -28,7 +28,7 @@ Tach is:
## Commands

- [`tach mod`](../usage/commands#tach-mod) - Interactively define module boundaries.
- [`tach check`](../usage/commands#tach-check) - Check that boundaries are respected.
- [`tach check`](../usage/commands#tach-check) - Check that boundaries and interfaces are respected.
- [`tach sync`](../usage/commands#tach-sync) - Sync constraints with the actual dependencies in your project.
- [`tach show`](../usage/commands#tach-show) - Visualize your project's dependency graph.
- [`tach report`](../usage/commands#tach-report) - Generate a dependency report for a file or module.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/why-tach.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ With Tach, you can:
3. Enforce those dependencies ([`tach check`](../usage/commands#tach-check))
4. Visualize those dependencies ([`tach show`](../usage/commands#tach-show) and [`tach report`](../usage/commands#tach-report))

You can also enforce a [strict interface](../usage/strict-mode) for each module, and [deprecate dependencies](../usage/deprecate) over time. This means that only members that are listed in `__all__` can be imported by other modules.
You can also enforce a [public interface](../usage/interfaces) for each module, and [deprecate dependencies](../usage/deprecate) over time. This means that only members that are listed in `__all__` can be imported by other modules.
4 changes: 2 additions & 2 deletions docs/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@
"pages": [
"usage/commands",
"usage/configuration",
"usage/caching",
"usage/strict-mode",
"usage/interfaces",
"usage/deprecate",
"usage/tach-ignore",
"usage/unchecked-modules",
"usage/vscode",
"usage/caching",
"usage/faq"
]
},
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ If you use the [pre-commit framework](https://github.com/pre-commit/pre-commit),
```yaml
repos:
- repo: https://github.com/gauge-sh/tach-pre-commit
rev: v0.14.4 # change this to the latest tag!
rev: v0.15.0 # change this to the latest tag!
hooks:
- id: tach
```
Expand Down
46 changes: 40 additions & 6 deletions docs/usage/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Aside from running `tach mod` and `tach sync`, you can configure Tach by creatin

This is the project-level configuration file which should be in the root of your project.

`modules` defines the modules in your project, and accepts the keys described [below](#modules).
`modules` defines the modules in your project - [see detais](#modules).

`interfaces` defines the interfaces of modules in your project - [see details](#interfaces).

`exclude` accepts a list of directory patterns to exclude from checking. These are treated as regex/glob paths (depending on `use_regex_matching`) which match from the beginning of a given file path. For example: when using glob matching `project/*.tests` would match any path beginning with `project/` and ending with `.tests`.

Expand Down Expand Up @@ -48,7 +50,6 @@ root_module = "allow"
[[modules]]
path = "tach"
depends_on = []
strict = true

[[modules]]
path = "tach.__main__"
Expand All @@ -58,14 +59,12 @@ depends_on = [{ path = "tach.start" }]
[[modules]]
path = "tach.errors"
depends_on = []
strict = true
utility = true

[[modules]]
path = "tach.parsing"
depends_on = [{ path = "tach" }, { path = "tach.filesystem" }]
visibility = ["tach.check"]
strict = true

[[modules]]
path = "tach.check"
Expand All @@ -74,7 +73,18 @@ depends_on = [
{ path = "tach.filesystem" },
{ path = "tach.parsing" },
]
strict = true

[[interfaces]]
expose = ["types.*]
[[interfaces]]
expose = [
"parse_project_config",
"dump_project_config_to_toml",
]
from = [
"tach.parsing",
]

...

Expand All @@ -95,14 +105,38 @@ Each module listed under the `modules` key above can accept the following attrib
- `path`: the Python import path to the module (e.g. `a.b` for `<root>/a/b.py`)
- `depends_on`: a list of the other modules which this module can import from (default: `[]`)
- `visibility`: a list of other modules which can import from this module (default: `['*']`)
- `strict`: enables [strict mode](strict-mode) for the module (boolean)
- `utility`: marks this module as a **Utility**, meaning all other modules may import from it without declaring an explicit dependency (boolean)
- `unchecked`: marks this module as [**unchecked**](unchecked-modules), meaning Tach will not check its imports (boolean)

<Note>
Tach also supports [deprecating individual dependencies](../usage/deprecate).
</Note>

## Interfaces

Public interfaces are defined separately from modules, and define the imports that are allowed from that module.

For example, if a module should expose everything from a nested 'services' folder, the config would look like:

```toml
[[interfaces]]
expose = ["services.*"]
from = ["my_module"]
```

More specifically:

- `expose`: a list of regex patterns which define the public interface
- `from` (optional): a list of regex patterns which define the modules which adopt this interface

<Note>
If an interface entry does not specify `from`, all modules will adopt the interface.
</Note>

<Note>
A module can match multiple interface entries - if an import matches _any_ of the entries, it will be considered valid.
</Note>

## The Root Module

By default, Tach checks all of the source files beneath all of the configured [source roots](#source_roots).
Expand Down
1 change: 0 additions & 1 deletion docs/usage/deprecate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ A dependency can be marked as `deprecated` - this means that the intention is to
Given modules called 'core' and 'parsing':

```toml
# tach.toml
[[modules]]
path = "parsing"
depends_on = [
Expand Down
7 changes: 7 additions & 0 deletions docs/usage/faq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ Tach allows marking these modules as **Utilities**, which means they can be used

[See more information on configuration here.](./configuration)

### How can I define a public interface for a module?

[Public interfaces](./interfaces) are defined in [tach.toml](./configuration#interfaces), and restrict the imports which are allowed from a given module.
This is useful when you want to expose a stable API from a module and prevent other modules from becoming deeply coupled to its implementation details.

[See more information on configuration here.](./configuration)

### Are conditional imports checked?

Tach will check all imports in your source files, including those which are called conditionally.
Expand Down
58 changes: 58 additions & 0 deletions docs/usage/interfaces.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Interfaces
---

A module can adopt a public interface by matching interface rules in [`tach.toml`](./configuration#interfaces).

## How does it work?

When Tach is checking imports from a module with a [public interface](./configuration#interfaces), it will verify that the import matches one of the `expose` patterns.
This prevents other modules from becoming coupled to implementation details, and makes future changes easier.

## Example

Given modules called 'core' and 'domain', we may have `tach.toml` contents like this:

```toml
[[modules]]
path = "domain"
depends_on = [
{ path = "core" }
]

[[modules]]
path = "core"
depends_on = []

[[interfaces]]
expose = ["get_data"]
from = ["core"]
```

Then, in `domain.py`, we may have:

```python
from core.main import DataModel # This import fails

DataModel.objects.all()
```

This import would **fail** `tach check` with the following error:

```shell
❌ domain.py[L1]: Module 'core' is in strict mode. Only imports from the public interface of this module are allowed. The import 'core.main.DataModel' (in module 'parsing') is not public.
```

In this case, there is a public interface defined in `tach.toml` which includes a service method to use instead.

```python
from core import get_data # This import is OK

get_data()
```

`tach check` will now pass!

```bash
✅ All module dependencies validated!
```
68 changes: 0 additions & 68 deletions docs/usage/strict-mode.mdx

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "tach"
version = "0.14.4"
version = "0.15.0"
authors = [
{ name = "Caelean Barnes", email = "caeleanb@gmail.com" },
{ name = "Evan Doyle", email = "evanmdoyle@gmail.com" },
Expand Down
2 changes: 1 addition & 1 deletion python/tach/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations

__version__ = "0.14.4"
__version__ = "0.15.0"

__all__ = ["__version__"]
8 changes: 7 additions & 1 deletion python/tach/extension.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def check_computation_cache(
def update_computation_cache(
project_root: str, cache_key: str, value: tuple[list[tuple[int, str]], int]
) -> None: ...
def parse_project_config(filepath: Path) -> ProjectConfig: ...
def parse_project_config(filepath: Path) -> tuple[ProjectConfig, bool]: ...
def parse_interface_members(source_roots: list[Path], path: str) -> list[str]: ...
def dump_project_config_to_toml(project_config: ProjectConfig) -> str: ...
def check(
Expand Down Expand Up @@ -109,6 +109,11 @@ class ModuleConfig:
def __new__(cls, path: str, strict: bool) -> ModuleConfig: ...
def mod_path(self) -> str: ...

class InterfaceConfig:
expose: list[str]
# 'from' in tach.toml
from_modules: list[str]

CacheBackend = Literal["disk"]

class CacheConfig:
Expand All @@ -130,6 +135,7 @@ class RulesConfig:

class ProjectConfig:
modules: list[ModuleConfig]
interfaces: list[InterfaceConfig]
cache: CacheConfig
external: ExternalDependencyConfig
exclude: list[str]
Expand Down
Loading

0 comments on commit 689a505

Please sign in to comment.