Disentangle the dependencies between your packages.
Dec 15, 2022
As soon as your application gets a bit bigger, you'll probably divide it into subsystems. In Python, this usually means packages. How well the boundaries between those packages are defined and how the communication flows between them greatly influences the maintainability of your project.
In this blog post, we're going to:
Following this example, you can create architecture rules for your own projects.
We're using a FastAPI application as an example. It stores its data in a PostgreSQL database via SQLAlchemy. It's divided into 4 packages:
app.api
app.core
app.db
app.schemas
How do these packages communicate with each other? And with the external libraries powering them?
This might take many different forms. One possible architecture is this:
But it can also look something like this:
You can make things even more complicated by introducing circular dependencies:
While Python doesn't allow circular dependencies between modules, it won't stop you from introducing circular dependencies between packages.
The first architecture looks more simple. And this has some advantages:
Now, we have a picture of how these packages should communicate. Let's define some rules to achieve this architecture. First, we'll establish some rules on how the packages of our application communicate with each other:
api
.api
depends core
.core
depends on db
.Let's turn to the external dependencies. We want to ensure that each of them is accessed only by the relevant package of our system:
api
db
models
Note that you probably don't want to define such a rule for every single dependency. But for libraries that are a crucial part of the architecture, it's a good idea to keep the interface clean.
So far, we've defined the dependency rules of our system: in a picture and in some sentences. Let's see how it matches up with the reality of our system. 😏 For that, we'll take the rules from the previous section and translate them into code.
For this translation, we'll use the tool Sourcery Rules Generator It creates Sourcery's custom rules in YAML format based on your input. You can install it from PyPI:
pip install sourcery-rules-generator
Let's start with the first rule:
No other package depends on api
.
To create such a rule about dependencies, run the command:
sourcery-rules dependencies create
You'll see:
Provide the parameters, so that the dependency rules can be generated:
app.api
.)
Now, you'll get an output with 2 rules in YAML format:
import
statementsfrom ... import
statementsThese rules detect if app.api
or any of its subpackages gets imported within
your project.
rules:
- id: dependency-rules-app-api-import
description: Do not import `app.api` in other packages
pattern: import ..., ${module}, ...
condition: module.matches_regex(r"^app\.api\b")
paths:
exclude:
- app/api/
- tests/
tags:
- architecture
- dependencies
- id: dependency-rules-app-api-from
description: Do not import `app.api` in other packages
pattern: from ${module} import ...
condition: module.matches_regex(r"^app\.api\b")
paths:
exclude:
- app/api/
- tests/
tags:
- architecture
- dependencies
What do these rules contain?
app.api
package.app.api
package in itself
and in tests.architecture
and dependencies
tags. This way, we can check for all
the architecture rules together.You can copy these rules into your projects .sourcery.yaml
config file. For
that it's recommended to use the --plain
option:
sourcery-rules dependencies create --plain
Now, you can run the sourcery review
command of the
Sourcery CLI to
check for these rules:
sourcery review --enable architecture .
Now, we can create similar Sourcery custom rules for our other internal dependency rules.
Only api
depends core
.
Again, we run the same command:
sourcery-rules dependencies create
This time, we fill both parameters:
app.core
is the packageapp.api
is the only package importing it
Again, 2 rules are created:
rules:
- id: dependency-rules-app-core-import
description: Only `app.api` should import `app.core`
pattern: import ..., ${module}, ...
condition: module.matches_regex(r"^app\.core\b")
paths:
exclude:
- app/core/
- tests/
- app/api/
tags:
- architecture
- dependencies
- id: dependency-rules-app-core-from
description: Only `app.api` should import `app.core`
pattern: from ${module} import ...
condition: module.matches_regex(r"^app\.core\b")
paths:
exclude:
- app/core/
- tests/
- app/api/
tags:
- architecture
- dependencies
You can also create similar rules ensuring that only app.core
should import
app.db
.
Let's turn to the rules regarding the external dependencies:
api
depends on FastAPIdb
depends on SQLAlchemymodels
depends on PydanticWe can create these rules the same way we did for the internal dependencies.
This time, the package
won't be a package in our project, but an external
dependency.
Only app.api
imports FastAPI.
sourcery-rules dependencies create
This time, we fill both parameters:
fastapi
is the packageapp.api
is the only package importing it
Again, the output shows 2 rules:
rules:
- id: dependency-rules-fastapi-import
description: Only `app.api` should import `fastapi`
pattern: import ..., ${module}, ...
condition: module.matches_regex(r"^fastapi\b")
paths:
exclude:
- fastapi/
- tests/
- app/api/
tags:
- architecture
- dependencies
- id: dependency-rules-fastapi-from
description: Only `app.api` should import `fastapi`
pattern: from ${module} import ...
condition: module.matches_regex(r"^fastapi\b")
paths:
exclude:
- fastapi/
- tests/
- app/api/
tags:
- architecture
- dependencies
You can create rules in the same way for the 2 other external dependencies as well:
app.db
imports SQLAlchemyapp.models
imports PydanticNow, we have 12 Sourcery custom rules to express our 6 architecture rules. We
can run all of them with the sourcery review
command in the Sourcery CLI:
sourcery review --enable architecture .
Following the examples above, you can create rules for dependencies in your own projects. The recommended steps:
If you're working on a huge application, don't try to tackle the whole architecture at once. Pick a subset of packages and go through the 3 steps with this smaller scope.
You can also check out the how-to guide Establish Rules for Dependencies Between Your Packages in the Sourcery docs.
Have you created architecture rules for your system? Do you have any questions, thoughts on this? Let us know.
Reach out at hello@sourcery.ai or on Twitter @SourceryAI. Join the Sourcery Discord Community.