Zügel Just Learned Python

With the 26.4.1 release of the Zügel MCP server we added Python as the second supported language after Java. This article only explains the Python-specific features. If you want to learn what Zügel is and what it can do for you, please read our introduction article. For the initial setup of Zügel please read this article.

Identifying things

A Python componentId is module/path/to/file — the source file’s location under its source root, with the extension stripped. shop/app/orders/repository is src/app/orders/repository.py in the module shop, whose source root is src — the module name comes first, then the path under the source root. That is the same rule Zügel uses everywhere; it is just more visible here, because Python has no fully-qualified type name that identifies a file.

locate_fqn still works, and resolves three kinds of name:

you haveit resolves
pkg.archive.UUIDa class
pkg.archive.read_headera module-level function
pkg.archivethe module itself — the file

That third row has no Java counterpart. In Java a file’s identity already is a class name; in Python a module is a file, and pkg.archive is what an import statement actually writes. It is usually the one you want.

Python Attribute Retrievers

Most .arc patterns match on names and paths, and those work identically. Like for Java we offer a few attribute retrievers that will allow you to match components by other criteria like class hierarchy or decorators. 

PythonTypeOf — anything deriving from a given base

include "PythonTypeOf: pydantic.BaseModel"
include "PythonTypeOf: airflow.sdk.bases.operator.BaseOperator"

It matches a Python file if any of the classes in the file have a base class matching the Pattern.

Bases outside your project count. pydantic.BaseModel, enum.Enum, builtins.Exception — you can write a rule about all of them, which matters because that is where a framework’s roles usually live. What Zügel cannot do is see through them: it does not parse installed packages, so a class deriving from BaseModel is found, while one deriving from some third-party class that itself derives from BaseModel is not. (Java has the same boundary at the edge of its classpath.)

PythonHasDecorator — anything carrying a given decorator

include "PythonHasDecorator: airflow.decorators.task"
include "PythonHasDecorator: app.get"

This is Python’s answer to JavaHasAnnotation, with two differences.

It is not called PythonHasAnnotation, deliberately: in Python an annotation is a type hint (x: int), so borrowing Java’s word would name the feature after something else entirely.

And it matches decorators on functions as well as classes — which is the whole point. Decorated classes are rare in Python; decorated functions are everywhere, and they are what carries the role: @app.get, @task, @event.listens_for, @registry.mapped.

Both spellings match, and you will want both:

  • @task, written after from airflow.decorators import task, resolves to airflow.decorators.task — so a rule written that way keeps working after someone writes import task as t.
  • @app.get resolves to nothing at all, because app is an instance, not a type. There is no fully-qualified name to write. So Zügel also matches the decorator exactly as written, and PythonHasDecorator: app.get does what it looks like.

Known limits

Worth knowing before you start, so nothing surprises you:

  • Dynamic Python is invisible, and says so. A module using PEP 562’s lazy __getattr__ to expose names — Airflow’s top-level package is the well-known example — cannot be resolved statically by anything. Those references are reported as unresolved rather than silently dropped.
  • The class hierarchy stops at your project’s edge, as described above.

Leave a Reply

Your email address will not be published. Required fields are marked *