Skip to content
rlsbl.workspace_graph
On this page

Dependency graph builder for monorepo workspaces that parses project manifests and provides topological sorting for ordered operations.

#rlsbl.workspace_graph

#rlsbl.workspace_graph

Dependency graph builder for monorepo workspaces that parses project manifests and provides topological sorting for ordered operations.

#WorkspaceScanner

Protocol for pluggable workspace dependency scanners.

#scan

python
def scan(self, project_dir: str, workspace_names: set[str]) -> list[Dependency]

Scan a project directory for intra-workspace dependencies.

project_dir: absolute path to the project directory. workspace_names: set of all workspace project names (raw, unnormalized). Returns a list of Dependency namedtuples for deps found within the workspace.

#CycleError

Raised when the workspace dependency graph contains a cycle.

#ManifestScanError

A manifest a scanner could not read or parse.

Raised by the scanners rather than swallowed into an empty dependency list. A failed scan contributes NO edges, and edges are what a dependent's CI paths filter is derived from, so a swallowed failure quietly NARROWS that filter: the dependent stops reacting to changes in its dependency's territory, its job concludes skipped on the very commit a release tags, and the freshness check -- re-deriving from the same broken manifest -- agrees the narrowed router is fresh.

:class:WorkspaceGraph catches it, keeps the tolerant behaviour its rendering consumers rely on (a warning on stderr, no edges from that manifest, the rest of the workspace still answerable), and records it on :attr:WorkspaceGraph.scan_errors. A consumer whose output must never be narrower than the truth reads that attribute and refuses.

#acknowledged_by

python
def acknowledged_by(self, project) -> bool

Has project already stated by hand what this scan could not read?

Always False here. A manifest nobody could READ withheld its whole dependency section, and a depends_on declaration says nothing about what the unread file would have added -- the operator wrote the declaration for the edges they know about, not to certify a file they cannot see either. Only a failure whose class is confined to declarations an explicit edge can replace overrides this; see :meth:UnrecognizedGradleDependencyError.acknowledged_by.

#UnrecognizedGradleDependencyError

A Gradle file that parsed, carrying a declaration nobody could read.

The same failure as :class:ManifestScanError wearing different clothes: a dependency declared through a variable, a helper function, or a catalog alias with no catalog behind it exists in the build and not in the graph, so every filter derived from the edges is NARROWER than the workspace -- and the freshness check, re-deriving from the same line, agrees the narrowed router is fresh.

It therefore travels the road the read failures already built: the scanner raises, :class:WorkspaceGraph keeps the edges the file DID declare and records the failure, the rendering consumers still get their answer with a warning, and a consumer that must never narrow refuses. The remedy it names is the one no scanner has to recognize -- declaring the edge in the member's depends_on -- and that remedy really clears the refusal, see :meth:acknowledged_by.

#acknowledged_by

python
def acknowledged_by(self, project) -> bool

Does project's workspace entry DECLARE a depends_on key?

The declaration is the operator's own statement of that member's workspace edges, and it is what the refusal asks for. With it present -- a list of names, or an explicit empty list -- the member's edges no longer depend on this file being readable: the explicit ones are in the graph whatever the line says, so an unreadable declaration can no longer make the derived filter narrower than the workspace.

Presence is the whole test, not content: an empty list is a member saying "no workspace edges", which is an answer. Absence is not, so a member without the key keeps the hard refusal.

#_parse_pypi_dep_name

python
def _parse_pypi_dep_name(dep_string)

Extract the package name from a PEP 508 dependency string.

Handles forms like: - "requests>=2.0" - "my-lib[extra]>=1.0" - "foo @ file:///path/to/foo" - "foo @ {root:uri}/path" Returns (name, is_path_dep, constraint) where constraint is the version specifier string or the full @ URI for path deps.

#PypiScanner

Scan pyproject.toml for intra-workspace PyPI dependencies.

#scan

python
def scan(self, project_dir: str, workspace_names: set[str], *, pypi_name_map: dict[str, str] | None=None) -> list[Dependency]

#NpmScanner

Scan package.json for intra-workspace npm dependencies.

#scan

python
def scan(self, project_dir: str, workspace_names: set[str]) -> list[Dependency]

#DartScanner

Scan pubspec.yaml for intra-workspace Dart/Flutter dependencies.

#scan

python
def scan(self, project_dir: str, workspace_names: set[str]) -> list[Dependency]

#MavenScanner

Scan Gradle (Kotlin/Groovy) and Maven (pom.xml) for intra-workspace JVM dependencies.

#scan

python
def scan(self, project_dir: str, workspace_names: set[str], *, workspace_root: str | None=None) -> list[Dependency]

#_load_catalog

python
def _load_catalog(self, project_dir: str, workspace_root: str | None) -> dict[str, str]

Load Gradle version catalog, returning alias -> 'group:artifact' map.

Checks workspace root first (shared catalog), then project dir. Results are cached per workspace root.

#_parse_version_catalog

python
def _parse_version_catalog(catalog_path: str) -> dict[str, str]

Parse libs.versions.toml and return alias -> 'group:artifact' map.

#_declares_no_workspace_edge

python
def _declares_no_workspace_edge(cls, arg: str) -> bool

Is arg a dependency argument that cannot name a workspace member?

The unrecognized-declaration failure is a hard error for the consumers that refuse on it, so it must fire only on a line that plausibly declares a dependency the scanner failed to parse. These forms are parsed fine; they simply declare no edge.

#_blank_comments

python
def _blank_comments(content: str) -> str

content with every comment blanked out, offsets left untouched.

Gradle's configuration names are ordinary English words, so a comment mentioning one (// implementation is intentionally omitted, // api docs: ...) reads exactly like a declaration to the line-oriented patterns below -- and reporting a comment as an unreadable dependency blocks a workspace whose build files are fine.

Comments are replaced character-for-character with spaces rather than removed, and newlines are kept, so every offset into the returned text addresses the same place in the original: the scanners match against this and render the reported line from the original, which keeps the operator's own text (comment included) in the message.

Quote-aware, because // inside a string literal is not a comment -- a repository URL is the common case.

#_line_at

python
def _line_at(content: str, offset: int) -> tuple[int, str]

(line number, source line) for the line offset falls on.

#_resolve_catalog_alias

python
def _resolve_catalog_alias(ref: str, catalog: dict[str, str]) -> str | None

Resolve a libs. reference to 'group:artifact' using the catalog.

Gradle normalizes dashes, underscores, and dots in alias names, so libs.someLib, libs.some-lib, libs.some_lib, libs.some.lib all refer to the same catalog entry.

#_scan_gradle_kts

python
def _scan_gradle_kts(self, filepath: str, workspace_names: set[str], catalog: dict[str, str] | None=None) -> list[Dependency]

Parse build.gradle.kts for dependency declarations.

#_scan_gradle_groovy

python
def _scan_gradle_groovy(self, filepath: str, workspace_names: set[str], catalog: dict[str, str] | None=None) -> list[Dependency]

Parse build.gradle (Groovy DSL) for dependency declarations.

#_scan_pom

python
def _scan_pom(self, filepath: str, workspace_names: set[str]) -> list[Dependency]

Parse pom.xml for elements.

#WorkspaceGraph

Directed dependency graph of intra-workspace project dependencies.

Construction is tolerant of a manifest it cannot read and of a Gradle file that parsed but declares a dependency in a form no scanner recognizes: the failure warns on stderr, contributes no edge for what could not be read, and the rest of the workspace is still answerable -- which is what monorepo impact, monorepo graph and monorepo status need on a half-broken tree.

Tolerance is only safe for a consumer that RENDERS the graph. A consumer that derives something narrowing from it cannot tell "this member has no dependencies" apart from "nobody could read this member's dependencies", and picking the first silently drops a real edge. Every failed scan is therefore recorded on :attr:scan_errors (a list of :class:ScanError), and such a consumer refuses when the list is non-empty -- see :class:rlsbl.router_filters.RouterFilters.

One failure class is exempt from the recording, never from the warning: a Gradle declaration no scanner recognizes, in a member whose workspace entry declares its own depends_on (:meth:ManifestScanError.acknowledged_by). That declaration is what the failure's remedy asks for, so honouring it here is what makes the remedy actually clear the refusal.

#dependencies

python
def dependencies(self, project_name)

Return list of Dependency namedtuples for intra-workspace deps.

#dependents

python
def dependents(self, project_name)

Return list of project names that depend on this project.

#topological_order

python
def topological_order(self)

Return project names in topological order (leaves first).

Raises CycleError if the graph contains cycles.

#has_cycles

python
def has_cycles(self)

Return True if the dependency graph contains cycles.

#transitive_deps

python
def transitive_deps(self, name, depth=None)

Return transitive dependency names in BFS discovery order.

Excludes the starting node. Optional depth limits traversal (None = unlimited, 0 = empty list). Raises KeyError if name is not in the graph.

#transitive_rdeps

python
def transitive_rdeps(self, name, depth=None, scope_filter=None)

Return transitive reverse-dependency names in BFS discovery order.

Excludes the starting node. Optional depth limits traversal (None = unlimited, 0 = empty list). Optional scope_filter restricts traversal to edges whose scope matches the given string. Raises KeyError if name is not in the graph.

#dep_count

python
def dep_count(self, project_name)

Return number of intra-workspace dependencies for a project.

#rdep_count

python
def rdep_count(self, project_name)

Return number of projects that depend on this project.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search