One of the core use cases for Tach is isolating a module such that you can't create dependencies for it. You often want to do this for a variety of reasons:
This is easy to set up and enforce with Tach!
Let’s use the Django library as an example. We’ll enforce isolation of the crypto
utils module. This is important, because the crypto
module is responsible for password hashing, salting, secure random string generation, timing attack prevention, and more.
If someone unintentionally created a new dependency for the crypto
module, changing that dependency could have massive downstream consequences. By enforcing this at the CI level with Tach, we can ensure that won’t happen.
pip install tach
We can do this with tach mod
, which will open up an interface to configure Tach:
First, let’s identify the python source root in the project. This is the level at which absolute imports within your project operate at. This defaults to the root of the project, but if it’s different, you can mark it with s
in tach mod
. In Django, it happens to be the root, so we’re good to go!
This also creates an implicit boundary named <root>
which we’ll use to validate dependencies of the module we’re looking to isolate.
Second, let’s identify the module which you wish to isolate. We can navigate to the module with the arrow keys, and mark this module with a boundary using enter
.
That’s it for tach mod
! Let’s save our configuration with ctrl + s
.
Now that we’ve defined our modules, let’s sync the existing dependency state!
Run tach sync
, and let’s see what we’ve got:
Uh oh! We can see that the crypto
module does have a dependency on the root, meaning that it imports other first party packages and utilizes them.
Let’s see what’s going on by running tach report django/utils/crypto.py
Here we can see that there are two outstanding dependencies for the crypto
module:
django.conf.settings
django.utils.encoding.force_bytes
By inspecting the code, we can see that both of these usages are reasonable. We have three options:
tach mod
and re-sync, allowing crypto
to depend on them.Each approach has its own tradeoffs, but in this case let’s go for #2. This will prevent further usages of the modules being introduced.
In django/utils/crypto.py:
The # tach-ignore
directive will tell Tach that these two imports, and only these two lines, are acceptable boundary violations.
Now that we’ve marked the explicit exceptions, we can fully isolate our crypto
module!
Let’s run tach sync
one more time and check out our tach.yml
config:
Success! The depends_on
key for django.utils.crypto
is empty. We can see that django.utils.crypto
has no first party dependencies (except for the ones we explicitly marked), and the <root>
does use the crypto
module as intended.
We can check this is working by trying to import anything else into the crypto module, and running tach check
:
The last step is to add tach check
to your CI pipeline. This will cause Tach to explicitly fail on any dependencies that are introduced that don’t map to your tach.yml
. It may also be useful to set a CODEOWNER on the tach.yml
file, to ensure that your configuration persists correctly.
This also shifts left architecture concerns for your entire development team, and makes your architecture easier to understand for new hires.
Tach also enables you to visualize your dependency graph, enforce boundaries between modules, as well as define strict interfaces for a given module. If there’s a feature you’d like to see in Tach, feel free to drop an issue or join the discussion on Discord!
If you liked this blog post, check out our deep dive on performance optimization with Rust!