pyTooling.Timeout is a new package: a Timeout is the current timeout of the
calling thread while its with block runs, as a WarningCollector is the current
collector. Code below in the call hierarchy asks Remaining() for the seconds
left - to hand them to a blocking call - and a loop calls RaiseIfExpired() to
abort itself with a TimeoutExpiredError, which carries the timeout and by how much its
deadline was overrun; Current() returns the timeout itself. A nested timeout
can only shorten the time left. The timeout is cooperative: nothing interrupts a thread from outside.
pyTooling.REST is a new package: a small client for JSON REST APIs, built on the standard library, so
nothing building on it drags an HTTP stack into every consumer.
A resource is addressed by its path below APIURL, which is a
URL, so the API is stated once instead of in every call.
GetJSONObject() reads a resource and says where its next page is;
PostJSONObject(), PutJSONObject(),
PatchJSONObject() and DeleteResource()
write one.
The answer’s media type is checked before it is read as a JSON object.
MediaType.Matches asks whether a Content-Type header names a
media type: the header’s parameters are ignored, the name is compared case-insensitively, and the RFC 6839
structured syntax suffix counts, so application/vnd.github+json is application/json.
The next page is read from the RFC 8288 Link header. One pointing outside the client’s own API is
rejected rather than followed, because the token is only sent to that API.
A transiently failing request - TRANSIENT_HTTP_STATUS, a timeout, or an unreachable
API - is tried again after an exponentially growing pause, or one as long as a Retry-After header
demands, capped at MAXIMUM_RETRY_AFTER. A request that isn’t idempotent - POST
and PATCH - is sent once, because repeating it can create or change a resource twice.
_Authorization() and _AddErrorNotes() are
what a client of one API overrides: the bearer scheme of RFC 6750 is the default, and a status means what
that API says it means.
JSONObject is the type of a JSON object, as json.loads() returns it. It is
imported from here: pyTooling.CI doesn’t re-export it, so a pipeline model loads no HTTP stack.
pyTooling.DataModel is a new package for the foundations of data models.
KeyValuePairsMixin attaches arbitrary key-value-pairs to an element, accessed like
a dictionary, and get() reads a key that may be missing. The
elements of pyTooling.CI, pyTooling.ControlFlow, pyTooling.Graph and
pyTooling.Tracing, and a Node, derive from it, and reject key-value-pairs given as
anything but a mapping.
Predicate is the type of a function selecting elements, Predicate[Vertex] -
the methods filtering what they iterate, reverse or remove use it.
pyTooling.CI models a CI pipeline independently of the service running it: a
PipelineGroup of Pipelines holding called
Workflows, Matrixes,
Jobs and Steps, with the times and an
Outcome on every element, and rules on those a definition can give them. Its
exceptions derive from CIError. An element’s Duration is a
timedelta, DurationInSeconds the same as a float.
Workflows, matrices, jobs and steps carry an ordered list of rules (RulesMixin):
a Rule holds a condition as written, a RuleOutcome deciding what
happens to the element if it matches, and key-value-pairs for what only a service’s rule has. The first
matching rule decides. A GitHub if: is one rule, GitLab’s rules: and workflow:rules are the list,
see Rules.
The elements of a workflow need each other: AddNeed() links
two siblings, records the reverse link in Dependents, and
rejects a need outside the group with NeedDependencyError; needs and dependents
can also be given when an element is created. Validate() checks a
completed pipeline for cycles and names one with NeedDependencyCycleError.
ToGraph() converts a pipeline into a Graph:
an element is a vertex whose ID and value are the element, a dependency an edge, a called workflow or a
matrix a subgraph linked from the group’s vertex. By default the graph and its subgraphs are reduced to their
transitive reduction; reduce=False keeps an edge per dependency.
A group keeps its elements of every kind in one sequence, in the order they were added, so a definition
iterates in file order. A matrix produces jobs or - MatrixWorkflow - called
workflows.
GitHub Actions and GitLab CI - stages, needs:, child and multi-project pipelines, parallel - map onto
it, see Services.
Every element carries arbitrary key-value-pairs, like the elements of pyTooling.Graph: given as
keyValuePairs when it is created, and accessed with the element’s dictionary operators -
job["runner.os"], in, del, len() and iteration over the keys. What an element contains
is counted, checked, looked up and iterated by named methods: ElementCount, ContainsElement,
GetElement and IterateElements for a group, StepCount, ContainsStep and IterateSteps for a
job, PipelineCount, ContainsPipeline and IteratePipelines for a pipeline group.
A matrix calling a reusable workflow - jobs named Tests (3.14) / Unit - is modelled as a
Matrix of MatrixWorkflow instances instead of
one called workflow per combination, so a trace groups them below a matrix timespan.
A matrix instance’s Dimensions maps each dimension’s name
to its value, in the matrix’ order; str() prints the values only - Test (ubuntu-26.04, 3.14).
A group iterates what it holds in the order it was queued, jobs and nested groups alike, rather than
jobs first and called workflows last. The sort is stable, so elements reporting no time keep the order
they were listed in.
A group’s times span what it holds - its jobs, and for a Workflow the matrices
and called workflows below it as well.
IterateElements() of a Workflow yields its jobs,
its matrices and the workflows it calls, so the containers one level below it are reachable without
asking for each kind separately. IterateJobs() remains the way to reach every
job below it. Because an element is placed in its group under its own name, it is asked for by that name:
pipeline.ContainsElement("UnitTesting").
QualifiedNameMixin reports an element’s qualified name as GitHub Actions names it -
Caller / Build (ubuntu-26.04). The mixin expects the field it walks, so mixing it into a class without
_parent is reported instead of failing with an AttributeError later.
pyTooling.ControlFlow is a new package describing the control flow graph of a function: a
ControlFlowGraph of BasicBlocks and
Edges. Its exceptions derive from ControlFlowError.
A graph has an entry and an exit block, and optionally an unwind block, through which an exception leaves the
function. A block is looked up by its ID: graph.GetBlock(3), graph.ContainsBlock(3).
An edge’s EdgeKind says why control continues with its destination: a
fall-through, a jump, an exception - to a handler or to the unwind block - or an abnormal exit like
exit(). A switch is a block with an edge per target. A block creates edges with
EdgeTo() and EdgeFrom().
Blocks and edges carry an optional execution count, e.g. from a code coverage report, and an optional value.
Graphs, blocks and edges carry key-value pairs, served by their dunder methods as in pyTooling.Graph.
ToGraph() converts it into a Graph,
with a copy of the key-value pairs and kind and count as key-value pairs on vertices and edges, which
pyTooling.Graph.GraphML writes as data elements.
pyTooling.CallGraph is a new package describing the call graph of a program: a
CallGraph of Functions and
Calls. Its exceptions derive from CallGraphError.
A function is looked up by its ID: graph.GetFunction("main"),
graph.ContainsFunction("main"). Its FunctionKind says if it’s defined in
the described code, defined elsewhere like a library function, or a placeholder for unknown functions, e.g. the
targets of an indirect call.
A call leads from the caller to the callee, so a recursion is a cycle. Its
CallKind says if the callee is named (direct) or determined at run time
(indirect). Two functions can be connected by several calls, e.g. one per call site. A function creates calls
with CallTo() and CallFrom().
Functions and calls carry an optional execution count, e.g. from a profiler, and an optional value. A function
can refer to its ControlFlowGraph, and a call to its call site, a block of that
graph. Call graphs, functions and calls carry key-value pairs, served by their dunder methods.
ToGraph() converts it into a Graph, with
a copy of the key-value pairs and kind, count and callsite as key-value pairs.
ThisClass is a sentinel for a class variable whose value is the class declaring
it.
A class variable naming the class it is declared in couldn’t be written, because the class doesn’t exist while
its body runs - it had to be assigned after the class statement. ClassVar[...] = ThisClass says it in the
body, and ExtendedType rebinds it once the class exists.
Only a variable the class declared is rebound; an inherited one keeps the value its own class resolved.
The value is read back from the class, so an __init_subclass__ that replaced it wins.
The sentinel is an empty class rather than a bare object, so a variable annotated as a type still
type-checks.
WithoutTrailingDelimiter() returns a path that doesn’t end in
ELEMENT_DELIMITER, and WithoutTrailingSlash() a URL whose path doesn’t -
a URL’s element delimiter is the slash. A trailing delimiter is an empty last element, so /api/v3/ and
/api/v3 are different paths although they usually name the same thing - and a path something is appended to
wants the latter. A path with none answers with itself.
URL / resource and path / resource compose - __truediv__() and
__truediv__(), each taking a string or a path on the right, the way
pathlib.PurePath does. The right side is a relative reference: its path goes
below the left side’s, and a string brings its own query and fragment, which the left side’s are not carried
into, the way RFC 3986 resolves one. A path that starts with the delimiter names its own root and replaces
the left side, as pathlib joins a path. A trailing delimiter on the left is dropped first, so composing
/api/ with things names /api/things and not an empty element between them. A string on a URL’s right
side is checked to be a relative reference: it names no authority (no leading //) and no scheme (no : in
the first path element, RFC 3986’s path-noscheme), and holds none of the characters the RFC forbids -
URL.Parse("https://example.org/api") / "https://elsewhere.org/things" raises instead of appending a URL to a
path.
⚠️ A path flavour names the type of its elements - ELEMENT_TYPE, beside ELEMENT_DELIMITER and
ROOT_DELIMITER. Parse() reads that instead of being handed the path and
element classes as parameters, so its signature is Parse(path, root=None) and a flavour needs no Parse of
its own - Path lost the one it had. A flavour outside pyTooling passing
``pathCls`` and ``elementCls`` has to drop them and declare ``ELEMENT_TYPE``.
Parse() strips ROOT_DELIMITER from an absolute path, not as many
characters as ELEMENT_DELIMITER is long. The two are the same in a URL, so nothing parses differently today,
but a path flavour marking its root differently - a drive letter, a host separated by a colon - would have lost
the wrong number of characters.
pyTooling.GenericPath.URL reports what it rejects, and raises
URLError for it.
URLError replaces the bare
ToolingException the module raised, so a consumer can catch a URL problem
without catching everything pyTooling raises. It still derives from it.
⚠️ Host rejects a host name that is None or empty with a
ValueError, as its doc-string always said; None was a TypeError.
A rejected URL holding a character RFC 3986 forbids - a space, a control character, <, |,
… - gets a note naming it and its percent-encoding, e.g. “Character ‘ ‘ is not allowed in a URL.
Write it percent-encoded as ‘%20’.”
Protocols gains ws, wss, ssh, sftp, git,
ldap and ldaps. A scheme missing from the enumeration makes a URL unparseable, so the list
grows as schemes are needed. sftp is carried by SSH rather than being
FTP plus TLS - that combination is FTPS.
IsEncrypted answers whether a scheme is secured, by TLS or
by SSH. Testing TLS alone answers “is this TLS” and
misses ssh and sftp, which are encrypted and carry no TLS flag.
tcp, udp and unix name an endpoint whose higher protocol is unspecified -
tcp://0.0.0.0:2375 as the Docker daemon writes it, unix:///var/run/docker.sock for a local
socket. They do not combine with the other schemes: http:// runs over TCP without saying so.
mqtt/mqtts, amqp/amqps, redis/rediss, mongodb and
postgres/postgresql are listed too.
A bracketed IPv6 literal is parsed as the host: https://[2001:db8::1]:8080/path gave a host of
None and a path of [2001:db8::1]:8080/path, while reassembling into the original string - so a
round-trip looked correct while the model was wrong.
Parse() checks its parameter: None raises a ValueError and a
value of another type a TypeError naming it, instead of the re module reporting “expected
string or bytes-like object” about neither the parameter nor the value.
An unknown scheme raises a ToolingException listing the known ones, where
ftpx://host used to raise KeyError: 'FTPX'.
A query parameter that is no key=value pair raises a ToolingException naming
it, where ?flag used to raise “not enough values to unpack”. A = inside a value is legal and no
longer an error - ?key=a=b parses as {"key": "a=b"}, where it used to raise “too many values to
unpack”.
pyTooling.MetaClasses
A class or a mixin-class can name the members it expects from wherever it ends up, with the new expects class
keyword argument. The contract is checked at class construction and reported on instantiation, like an abstract
class. An override expects only what its own marker names, a class fulfilling an expectation its base-class
missed can be instantiated with parameters, and the search for marked methods skips object.
A slotted class can be referenced weakly, with the new weakref class keyword argument (issue #49):
weakref=True adds __weakref__ to the class’ slots like any other field; a derived class inherits it, and
a mixin-class passes it on to the class it is mixed into.
pyTooling.Documentation is a new module holding the helpers that work on doc-strings.
splitDocString() splits a doc-string into its summary - the first
paragraph - and its body. Three unrelated features read a doc-string that way: the doc-string merge
strategies, a package’s short description, and a testcase’s names.
A summary is a single sentence, so its length is bounded by
MAXIMUM_SUMMARY_LENGTH. A longer first paragraph raises a
DocumentationError, because it is a body that lost its summary.
pyTooling.Testing
ApplicationTestcase no longer requires _RUNNABLE_MODULE: a program without a
__main__ module is tested through its entry point, and only
RunModule() asks for the module.
RunEntrypoint() and
RunModule() wait 60 seconds by default instead of 10. The timeout
guards against a hanging program; a cold Windows runner or PyPy on macOS needed longer than 10 seconds.
@testsuite and @testcase mark what a test runner
collects, so a testcase’s name stops carrying two unrelated jobs at once.
Both markers take a title, and both fall back to the doc-string: its summary becomes the summary, its body
becomes the description. A test item has four names - an ID, a title, a summary and a description.
pyTooling.Testing.PyTest is a new pytest plugin collecting what the markers mark. Node IDs are left
untouched, so test selection, pytest-xdist, --last-failed and IDE integration are unaffected. A marked
method of a unittest.TestCase class keeps its own name as its ID, though unittest collects
only methods named test*. The markers themselves aren’t collected as testcases, and a summary longer than
200 characters is left out of the report with a PytestCollectionWarning naming the item.
pyTooling.Testing.ReportWriter is a second plugin writing a test report format of our own,
opt-in through --pytooling-xml=PATH and additional - it runs in the same session as --junit-xml, so
a pipeline keeps the format its dashboard understands while the richer file appears beside it. Test suites
nest instead of being flattened into a dotted classname, every level can carry a title and a
description, and a description’s line breaks survive.
The format has a versioned schema, TestReport-v0.1.xsd, shipped as a package resource and published
in the documentation.
The names of every test suite level reach the JUnit report too, as test suite properties keyed by the
level’s dotted path. The innermost key is the testcase’s classname and every outer one is a prefix of
it, which is what lets a reader join the two.
pyTooling.Licensing
28 more SPDX licenses, taking SPDX_INDEX from 4 to 32 - permissive, weak and strong copyleft, and public
domain, among them MIT-CMU, Pillow’s license. The -only/-or-later pairs are separate licenses, as
SPDX defines them, because PyPI has a distinct classifier for each.
The six Creative Commons 4.0 licenses (CC-BY-4.0, CC-BY-SA-4.0, CC-BY-NC-4.0,
CC-BY-ND-4.0, CC-BY-NC-SA-4.0, CC-BY-NC-ND-4.0) are among them, for documentation and media. PyPI
has no classifier for them, so asking one for its classifier raises a ValueError.
SPDX_INDEX is built from the licenses rather than repeating each identifier, so the two can no longer
disagree.
A License is hashable and compares equal to its SPDX identifier as a string,
so it can be a dictionary key and be looked up by what a user writes.
An SPDX license expression is a tree: LicenseExpression is the base of
SPDXLicense, AndOperator,
OrOperator, WithOperator and
OrLaterOperator, so Apache-2.0 OR MIT or GPL-2.0-only WITH
Classpath-exception-2.0 can be stated. A malformed expression raises
LicenseExpressionError.
LICENSE_TEXT_URLS and LICENSE_URLS name the GNU licenses’ texts in every format gnu.org publishes.
pyTooling.Attributes
pyTooling.Attributes.ArgParse
splitFormat() splits an option’s value of the form
[<format>:]<file> into the format and the file, for any program declaring an option of that shape. A
format is more than one character long and holds no path separator, so a Windows drive and a colon deeper
down a path stay part of the path. The formats are a StringEnum, and a value
naming none gets its DEFAULT - or raises a ValueError, if the enumeration declares none.
NamedAndValuedArgument and NamedTupledArgument are exported from
pyTooling.Attributes.ArgParse.Argument.
pyTooling.TerminalUI
pyTooling.Diagram
A new namespace for data models of diagrams, which describe a picture without drawing it.
pyTooling.Diagram.Gantt describes a Gantt chart - a Diagram of
Rows of Bars on a time scale. An
element is created with its parent and knows the diagram it belongs to, and a bar reports where it is three
ways: as times, as the distance from the diagram’s origin, and as the distance from its row.
pyTooling.Tracing
Span and Event are
TraceElements - a name, the timespan enclosing them, and their
attributes, which each of them declared and validated itself before.
A trace’s elements take their attributes from KeyValuePairsMixin and answer
its get(), which reads an attribute that may not be there and
returns a default value instead of raising.
⚠️ Iterating a trace’s element yields the attribute keys;
IterateAttributes() yields the (key, value) pairs that the
iteration yielded before.
A software execution trace exports itself as OTLP/JSON - ToJSON(),
ToJSONString() and WriteJSONFile(). One
format reaches both usual destinations: an OpenTelemetry collector accepts OTLP natively, and Jaeger has
accepted it since v1.35.
The document is typed rather than a mapping of Any: twelve TypedDict
classes name the OTLP messages they encode, from OTLPDocument down to
OTLPAnyValue.
A trace and each of its timespans draw their identifiers when they are constructed, so exporting one
trace twice reports the same traceId, and TraceID can be handed to
another process. A timestamp is exact to the microsecond a datetime holds.
An attribute’s value is a AttributeValue - the types OTLP’s AnyValue carries,
nested as deeply as needed. A value of any other type is rejected rather than stringified.
A trace and its timespans can be constructed with recorded times - beginTime and endTime - for
timespans measured elsewhere, e.g. by a CI service. Without them, a timespan is timed by its
with-statement as before.
Every kind of timespan is a class: PipelineTrace,
WorkflowSpan, MatrixSpan,
QueuedSpan, JobSpan and
StepSpan. Each names its kind in KIND and takes the conventions’
attributes as parameters, so a reader states values and never a key, and an unknown value sets no attribute.
The classes are service-independent; a service’s reader derives flavours that build themselves from its
model. Everything below the trace derives from the abstract TaskSpan, because
it is a task in the conventions’ sense.
The attribute keys are namespaces nested the way the keys themselves are, instead of a flat block of module
constants: OTLP for OpenTelemetry’s conventions and
CI for what pyTooling adds, so OTLP.CICD.Pipeline.Task.Run.ID spells cicd.pipeline.task.run.id. The closed value sets are enumerations -
SpanKind and Result.
A trace renders as a Gantt chart: GanttLayout arranges the timespans
in rows independently of a drawing library, and a Renderer draws what it
arranged. MatplotlibRenderer writes the chart as SVG, PNG or
PDF; matplotlib is installed by the new extra pyTooling[diagram]. Everything no drawing library decides -
the categories’ colors and the legend’s texts - is on the base-class, so a second backend repeats none of it. The
layout’s Duration - the end of the last bar - is a timedelta, DurationInSeconds the
same as a float.
A Gantt chart written as SVG can be collapsible: a click on a called workflow or a job hides or shows the
rows below it.
A timespan left by an exception records it - Exception - and the exception
propagates as before. The OTLP/JSON export reports such a span with STATUS_CODE_ERROR
(OTLPStatus); a failed span looked like a successful one.
pyTooling.Packaging
DescribePythonPackage() declares consoleScripts, guiScripts and
pytestPlugins, each knowing the entry point group it belongs to. Previously only console_scripts was
reachable.
⚠️ A package’s short description is the first paragraph of its module doc-string, so a package is described
in one place instead of two. The paragraph is folded into a single line and emphasis around the whole of it is
removed, because nothing renders ReST where a short description is displayed. Without description and without
a module doc-string, DescribePythonPackage() raises a
PackagingError; description moved behind the required parameters.
The module raises its own PackagingError, so a caller can catch what this module
reports without catching everything derived from ToolingException.
⚠️ DescribePythonPackage() leaves requirements with a direct reference (name @
URL) out of every extra and reports each as a critical DirectReferenceWarning to a
WarningCollector: the Python Package Index rejects a package whose metadata contains
one. pyTooling itself names pyTooling.Sphinx, which isn’t on PyPI yet, by its repository in
doc/requirements.txt; its doc and all extras carried it, so v10.0.0-rc1 couldn’t be uploaded.
Base requirements are kept. A setup.py with such an extra must collect the warnings, otherwise
UnhandledCriticalWarningError stops the build; pyTooling’s own collects and prints
them.
pyTooling.Common
getTypeName() returns the name of an object’s type without its module, the counterpart
of getFullyQualifiedName() for exception messages.
parseISO8601Timestamp() parses an ISO 8601 timestamp. Whether a timestamp carrying no
UTC offset stays naive is the caller’s decision, given as defaultTimeZone.
StringEnum is a StrEnum that converts a string to the member of
that value: Parse() rejects a non-string with a TypeError and an
unknown value with a ValueError listing the values it accepts, and answers a missing value with the
member the enumeration declares as DEFAULT - an alias, so it isn’t iterated - or with None where
there is none. MediaType, SpanKind and
Result derive from it.
pyTooling.LinkedList and pyTooling.Graph
Neither module raises its own base exception any more. LinkedList gained five specific errors and
Graph three, so 20 raise sites name what went wrong.
Graph.IterateTransitiveEdges names the edges a
longer path already implies, and RemoveTransitiveEdges() removes them, which
leaves the graph’s transitive reduction. A graph and each of its subgraphs are reduced separately.
IterateTransitiveEdgesWithPath() yields each implied edge with the path implying
it.
AnnotateTransitiveEdges() marks them instead: an edge’s
Kind becomes EdgeKind Direct or
Transitive, and with keyName a transitive edge gets the path implying it as a key-value-pair.
Edges and links have a kind: EdgeKind / LinkKind,
Default unless edgeKind / linkKind is given when they are created. A kind may be a member of an
enumeration of the user’s own.
The kind is a type parameter: EdgeKindType / LinkKindType, so
Edge[..., MyKind, ...] - after ID, value and weight - types Kind as
MyKind. The field annotations name the type parameters of Edge, Link, BaseGraph, Subgraph
and Graph in their declared order; value and weight were swapped, and some were missing.
pyTooling.Graph.GraphViz writes a graph in Graphviz’ DOT language, the counterpart of
pyTooling.Graph.GraphML: a Graph with subgraphs, nodes and edges,
attributes with dictionary syntax, record and HTML-like labels escaped where they are written, and
FromGraph() / FromTree() with
conversion methods a derived class overrides.
Graph.Delete deletes all subgraphs, views, components, vertices, edges
and links of a graph and disconnects them from each other, so reference counting frees them instead of the
cyclic garbage collector.
pyTooling.Tree
Node.RemoveChild and
RemoveChildren() remove children from a node, the counterparts of
AddChild() and AddChildren(). A removed child takes its
subtree along and becomes the root node of its own tree, like assigning None to its
Parent. A node, which isn’t a direct child, raises the new
NotAChildError, which carries it in Child.
Node.Delete deletes a node and its subtree and disconnects the nodes from
each other, so reference counting frees them instead of the cyclic garbage collector.
pyTooling.CLIAbstraction
pyTooling.Filesystem
Root.Delete deletes a scanned directory tree: all directories,
filenames, symbolic links and file objects, also those a collapse removed, are disconnected from each other, so
reference counting frees them instead of the cyclic garbage collector.
pyTooling.Dependency
ToGraph() converts a package dependency graph into a
pyTooling.Graph.Graph: a vertex per package version, with its license, release time and URLs as
key-value pairs, and an edge per dependency.
RequirementsFile reads a requirements file as a tree: a -r line is
followed, every file knows its Parent, its Root and its Hierarchy, and AllRequirements yields the
requirements in the order the files state them, the nearer statement winning. A cycle raises instead of being
read once and hidden. A file two files reference is read once; the second -r line is a
RequirementsFileReference to it. Each file read by
RequirementsFile.Parse measures its duration with a
Stopwatch, the files it references included:
AnalysisDuration.
PackageIndexError reports an HTTP error status of the package index other than 404
Not Found, caused by the HTTPError.
Resolver resolves requirements - given directly, from a requirements file
or from packages and their extras - into the newest matching releases of a package index, following their
requirements and the requirements of the extras they name. It takes a
ResolutionStrategy (lazy or eager), a limit (None is unlimited), a
PreReleasePolicy (a requirement naming a pre-release selects it also with
pre-releases excluded) and an environment to evaluate markers against (None evaluates none);
CurrentEnvironmentResolver uses the running interpreter’s. The result
is a tree of ResolvedRequirement objects, each with the selected release
or the reason there is none, and the selected releases are connected in the dependency graph. The downloads of
one level run in parallel. New: ExtraNotFoundError,
UnresolvedRequirementWarning, and the critical
PackageIndexFailureWarning for a server error or a failed connection of the
index. A resolution honours the current Timeout: requirements left unresolved when
it expires get the status TimeoutExpired and are
reported as the critical ResolutionTimeoutWarning.
pyTooling.Configuration
A configuration node answers the mapping and sequence protocols: a dictionary node’s keys(), values(),
items() and get(), a sequence node’s len() and indexing.
A Dictionary is iterated by keys and by items too -
IterateKeys(), IterateValues() and IterateItems(). Iterating a
node still yields its values.
pyTooling.Exceptions
pyTooling.Versioning
⚠️ PythonVersion understands the spellings PEP 440 normalizes, and keeps them:
10.0.0-pre1, -preview1 and c1 are release candidates, 10.0.0-dev, .dev and dev are
.dev0. A parsed version is written as parsed and compares and hashes like its normal form, which
Normalize() returns and Parse(..., normalize=True) returns
directly. SemanticVersion keeps the release level’s spelling too. Before, pre
and preview became a local version (10.0.0+pre1).
VersionExpression parses and matches what a requirement file writes, in four
dialects: the neutral base, PythonVersionExpression,
NPMVersionExpression and DebianVersionExpression. A
VersionConstraint is one comparison and a container of the versions satisfying it,
so membership is asked with in; PEP 440’s ~= and npm’s ^ and ~ derive an upper bound. An empty
expression matches every version. A constraint and an expression convert with ToVersionRange() where they
describe a single interval.
⚠️ A version number can carry an epoch - SemanticVersion.Parse("2:1.0.0"),
PythonVersion.Parse("2!1.0.0") - which orders before every other part. The separator is the class variable
_EPOCH_SEPARATOR, Epoch reads the epoch, and Parts.Epoch says it is
present.
A VersionRange can be open at either end: a bound of None is unbounded, so
>=1.0 is expressible.
Exceptions carry the object they are about, so a caller can react without parsing the message. The new
parameters are keyword-only and default to None, so an exception raised with a message only keeps working.
KeyNotFoundError carries the missing key or index in
Key.
UnsupportedValueTypeError carries the value the parser returned in
Value, its type in
ValueType. It is a TypeError too, as
KeyNotFoundError is a KeyError.
PathExpressionError carries the path expression in
PathExpression.
ProjectNotFoundError carries the project’s name in
ProjectName.
ReleaseNotFoundError carries the project’s name and the release’s version in
ProjectName and
Version.
PackageIndexError carries the project’s name and, for a release, its version in
ProjectName and
Version.
The warnings UnknownLicenseWarning and
ReleaseDetailsWarning carry the release’s project name and version in
ProjectName and Version, as UnresolvedRequirementWarning does.
BrokenRequirementWarning is the base of two warnings, each carrying what it is
about: UnparsableRequirementWarning the line of a requirements file (Line,
FilePath, LineNumber), UndeclaredExtraWarning the resolved requirement
naming an extra the selected release doesn’t declare (Requirement, Extra, Release). The new
UndeclaredExtraMarkerWarning reports a release’s requirements whose marker names
an extra the release doesn’t declare (Release, Requirements); it was a
BrokenRequirementWarning.
DirectReferenceWarning carries the requirement left out, its extra and the
requirements file (Requirement, Extra, FilePath);
UnannotatedFieldWarning the class’ name and the fields without a type annotation
(ClassName, FieldNames).
⚠️ PermissionWarning takes a message and carries the directory that couldn’t be
read in Directory, instead of taking the path as first
parameter and returning it as Path; it had no message.
DuplicateFieldInSlotsError carries the slot’s name and the name of the class
under construction in FieldName and
ClassName.
pyTooling.MetaClasses: the new common base InvalidBaseClassError of
BaseClassWithoutSlotsError,
BaseClassWithNonEmptySlotsError and
BaseClassIsNotAMixinError carries the base-class and the name of the class under
construction (BaseClass, ClassName); UnfulfilledExpectationError
the class and the missing members (IncompleteClass, MissingMembers);
IncompatibleMetaClassError the decorated class and its meta-class
(DecoratedClass, MetaClass); AbstractClassError the class and the methods
to override (AbstractClass, AbstractMethods).
pyTooling.Graph: EdgeNotFoundError and LinkNotFoundError
carry both vertices (Source, Destination), SameGraphError the graph
(Graph), CycleError the path closing the loop (Path).
pyTooling.CI: NeedDependencyError carries both elements (Element, Need),
NeedDependencyCycleError also the cycle (Cycle),
AlreadyInPipelineError the rule (Element).
DuplicateFunctionError carries the function’s ID (FunctionID),
DuplicateBlockError the block’s ID and kind (BlockID, Kind),
DuplicateEdgeError the edge connecting the blocks already (ExistingEdge).
NotInAListError carries the node (UnlinkedNode);
AlreadyInTreeError and NotInSameTreeError the other node
(OtherNode).
InterpolationError carries the value and the position of the malformed $
(Value, Position); LicenseExpressionError the expression and, for an unknown
license, its identifier (Expression, Identifier); NoSessionAvailableError
the project’s name and, for a release, its version (ProjectName, Version);
RequirementsFileNotFoundError the missing file (FilePath);
CircularRequirementsFileError the file read again and the -r line referencing it
(FilePath, ReferencedIn, LineNumber); UnknownPlatformError the value
the system reported (UnknownValue);
AttributeScopeError the attribute and the entity (AppliedAttribute,
Entity).
A lookup that finds nothing raises an exception that is a LookupError too, as
KeyNotFoundError is a KeyError:
ProjectNotFoundError, ReleaseNotFoundError,
EdgeNotFoundError and LinkNotFoundError.
RequirementsFileNotFoundError is a FileNotFoundError too.
⚠️ The four mixin-classes of pyTooling.GenericPath are renamed to the ***Mixin spelling
the rest of the package uses: PathMixIn → PathMixin, ElementMixIn
→ ElementMixin, RootMixIn →
RootMixin and SystemMixIn →
SystemMixin. They were the last four spelled MixIn, and the old names are
gone rather than kept as aliases - the same rule the renamed exceptions follow.
⚠️ Stopwatch.Duration,
Activity and Inactivity return a
timedelta instead of a float of seconds. Arithmetic with a number or a float format
spec (f"{sw.Duration:.3f}") raises a TypeError; use
DurationInSeconds, ActivityInSeconds
or InactivityInSeconds for the number.
⚠️ PermissionWarning takes a message and carries the directory:
PermissionWarning(path) becomes PermissionWarning(message, directory=...), and warning.Path becomes
warning.Directory.Path.
⚠️ Directory.ScanDuration,
AggregateDuration and
LayerCake.SlicingDuration return a
timedelta instead of a float of seconds; .total_seconds() gives the number.
⚠️ Span.Duration returns a timedelta instead of a
float of seconds; use DurationInSeconds for the number.
⚠️ Parse() reads the flavour’s ELEMENT_TYPE, so its signature is
Parse(path, root=None). A flavour outside pyTooling passing pathCls and elementCls drops them and
declares ELEMENT_TYPE.
⚠️ Host rejects a host name that is None or empty with a
ValueError. An empty name was accepted, and None raised a TypeError.
Parse() is unchanged.
⚠️ A membership test is named Contains***: Has*** asks for a property of the object itself, like
HasCycle(). Graph.HasVertexByID →
ContainsVertexByID(), Graph.HasVertexByValue →
ContainsVertexByValue() and GraphMLDocument.HasKey →
ContainsKey(), without aliases.
ContainsNode() is new in this release.
⚠️ An attribute’s scope is enforced: applying an attribute to a class, method or function its
Scope doesn’t allow raises
AttributeScopeError. Apply an attribute only where its scope allows it; pyTooling’s
own attributes keep the scope Any.
⚠️ In a class not built by ExtendedType,
GetFunctions() no longer returns marked methods;
GetMethods() does.
⚠️ RunEntrypoint() and
RunModule() merge environment into this process’s
environment instead of replacing it; a value of None removes a variable. A program given only the variables a
test named lost the rest - on Windows SystemRoot, without which it can’t open a network connection.
⚠️ 32 exception classes are renamed to the ***Error suffix, as PEP 8 asks for. Only
ToolingException, the package’s own base exception, keeps Exception. There are no
aliases of the old names, so an import or an except clause naming one has to be updated.
⚠️ Class variables with constant meaning are spelled in upper case, as INDENT and the exit codes already
are: TerminalApplication.HeadLine → HEADLINE, TerminalBaseApplication.Foreground →
FOREGROUND, Program._executableNames → _EXECUTABLE_NAMES, and
ApplicationTestcase._consoleScript and _runnableModule → _CONSOLE_SCRIPT and
_RUNNABLE_MODULE. There are no aliases: a subclass still setting an old name is no longer read, so rename it
in the class body.
⚠️ TerminalApplication._PrintHelp moved to ArgParseHelperMixin, which
owns the parsers it prints. An application calling it derives from both classes.
⚠️ TerminalBaseApplication.PrintMissingDependencyException() is renamed to
PrintMissingDependencyError(), after the exception it prints.
Rename the call; there is no alias.
⚠️ DescribePythonPackage() raises when it can find no description, instead of
publishing a package without one. Either pass description, or give the file named by sourceFileWithVersion
a module doc-string. The description parameter of both Describe*** functions moved behind the required
parameters, so a caller passing it positionally has to name it.
⚠️ A direct reference in an extra raises UnhandledCriticalWarningError, unless a
WarningCollector collects the DirectReferenceWarning: a
setup.py whose extras contain or might contain one wraps setup(...) in with WarningCollector() as
warnings:.
⚠️ A package described with DescribePythonPackage() carries no License ::
classifier; its license is the SPDX expression in license only. A consumer reading the license from the
classifiers reads license instead.
⚠️ @InheritDocString loses merge, summaryOnly and order; it takes
one DocStringMergeStrategy. merge=True is DocStringMergeStrategy.BaseFirst.
The default changes with it: @InheritDocString(Base) on an entity that has a doc-string replaced it and now
appends the base’s underneath.
⚠️ pyTooling.Configuration.Configuration takes only configFile, no root and parent. A
format implementation drops both arguments.
⚠️ A slotted secondary base-class has to be a mixin-class, also without fields:
ExtendedType raises BaseClassIsNotAMixinError.
Declare it with mixin=True or apply @mixin.
⚠️ A mixin-class’ slot that exists already raises DuplicateFieldInSlotsError: a
field of the class itself, a slot of the primary inheritance line, or a slot of a second mixin-class. Rename the
field, or drop one of the slots.
⚠️ A ClassVar naming a mixin-class’ slot raises
DuplicateFieldInSlotsError at class creation. Annotating it as ClassVar is not
the fix; rename the class variable, or drop the slot.
⚠️ Annotating __weakref__ or __dict__ raises an
ExtendedTypeError. Pass weakref=True instead of annotating __weakref__; a
class whose instances need a __dict__ uses slots=False.
⚠️ len() and iteration of a Node address its key-value-pairs, like []
and in: len(node) counts the pairs and iterating a node yields the keys. Both addressed the children
before. Use ChildCount, Children or
GetChildren() instead.
⚠️ Node takes two type parameters instead of four: Node[IDType, ValueType].
Its key-value-pairs come from KeyValuePairsMixin, keys are
Hashable and values Any. Node[a, b, c, d] raises a TypeError; drop the
last two arguments. Key-value-pairs given as anything but a mapping raise a TypeError.
⚠️ The elements of pyTooling.Graph take their key-value-pairs from
KeyValuePairsMixin: keys are Hashable, values Any.
The type parameters for keys and values are removed and the kinds of edges and links are type parameters:
Graph takes 11 instead of 23, Vertex and
Subgraph 11 instead of 17, View and
Component 11 instead of 19, Edge and
Link 4 instead of 5; adapt every subscript. The 16 ***DictKeyType and
***DictValueType type variables are removed. Key-value-pairs given as anything but a mapping raise a
TypeError.
⚠️ Iterating a Graph yields its keys, like len() already counted its
key-value-pairs. It yielded the vertices without an ID, followed by the IDs - not the vertices - of the others.
Use IterateVertices().
⚠️ The kind of an edge or link precedes keyValuePairs in EdgeToVertex(),
EdgeFromVertex, LinkToVertex, LinkFromVertex and the constructors of Edge and
Link: a key-value mapping passed positionally lands in the kind. Pass keyValuePairs
by keyword.
⚠️ Iterating a Trace, Span or
Event yields the attribute keys;
IterateAttributes() yields the (key, value) pairs the iteration
yielded before.
⚠️ Project.UpdateDetailsFromPyPIJSON
reports a release whose version it can’t parse as an UnsupportedVersionWarning,
a critical warning naming the project and the version number, e.g. pytz’ 2004d. It printed a line to the
console instead. The release is still skipped, but without a WarningCollector, reading
such a project raises UnhandledCriticalWarningError.
⚠️ BrokenRequirementWarning (with its derived warnings) and
ReleaseDetailsWarning are critical warnings. Without a
WarningCollector, RequirementsFile.Parse, reading a release’s details, downloading a project’s
release details and the Resolver raise
UnhandledCriticalWarningError (the warning is its __cause__) instead of dropping the
warning silently. Wrap such a call in a collector and handle the warnings it returns.
⚠️ ExceptionBase has no message field anymore. Read the message with
str(ex) or the new read-only property Message.
⚠️ RequirementsFile is read by
RequirementsFile.Parse: RequirementsFile(path)
→ RequirementsFile.Parse(path). The initializer reads no file anymore; it takes the path, the
requirements and referenced files as entries, and a keyword-only parent, so a tree can be built by hand.
A requirements file given in entries joins the tree; one referenced by another file already raises
AlreadyInRequirementsTreeError.
⚠️ A program found in PATH is the full path in Path and as
the first item of ToArgumentList(), e.g. /usr/bin/git instead of
git.
⚠️ str() of a Program or an argument is a command line escaped for the
platform, instead of every argument in double quotes. Code parsing or comparing that text adapts to it;
ToArgumentList() returns the arguments as a list.
⚠️ SemanticVersion.Parse("1.0a1") is written 1.0.a1, as parsed, not 1.0.alpha1. A version
constructed without a spelling is written as before.
⚠️ A dev or post part counts when it’s present: 1.0.dev0 == 1.0 and 1.0.post0 == 1.0 are False, as
PEP 440 orders them.
⚠️ A release level without a number is a pre-release: SemanticVersion.Parse("1.0.0-alpha") was a final
release with the postfix alpha; it is 1.0.0.alpha0.
⚠️ Every member of pyTooling.Versioning.Parts has a new value: Epoch = 1 takes the lowest bit, so
each of the other members doubled. Code using the members by name is unaffected; a persisted Parts value, or a
comparison against a literal, has to be re-read.
⚠️ VersionRange.__and__ no longer includes bounds the
operands exclude - see Bug Fixes.
pyTooling.TerminalUI imports without colorama: the color palette
FOREGROUND holds empty strings then, so a terminal
application writes uncolored text. Importing the module raised MissingDependencyError
without the terminal extra.
⚠️ Class variables with constant meaning, set in the class body or once per class from a class keyword argument,
are spelled in upper case. Besides the public ones listed under Breaking Changes: the patterns and names of
pyTooling.CLIAbstraction’s argument classes (_PATTERN, _NAME, _VALUE_PATTERN,
_FALSE_PATTERN, _PATTERN_WITH_VALUE), Attribute._SCOPE, GraphMLDocument._XML_NS and _XSI, and
ProcessInformation._PROCESS_STATUS_FILE and _PAGE_SIZE.
⚠️ ExceptionBase hands its message to Exception, so args is
(message, ) and repr() shows it, as for any Python exception. args was empty. The message field
and the __str__() override returning it are removed: the read-only property
Message returns args[0], and str(ex) is Python’s own.
MissingDependencyError is a ToolingException too, not
only an ImportError: except ToolingException catches a missing optional dependency now as well, and
name is the missing package. except ImportError catches it as before.
⚠️ An attribute’s scope is checked when the decorator runs (issue #384); a function defined in a class body is
recognized as a method by its qualified name, for any class. It was accepted silently and registered in a list the
scope said it could never hold. Scope is an instance property, and
GetAttributes() honours includeSubClasses=False.
⚠️ An attribute applied to a method files it under methods when the decorator runs, recognized by its qualified
name, instead of under functions until ExtendedType moved it - a search through
every function the attribute marks, per method. In a class not built by ExtendedType,
GetMethods() finds the method now, and
GetFunctions() no longer does.
⚠️ A package’s license is stated as an SPDX expression only; no License :: classifier is added.
setuptools deprecated them, and the expression was already there - license has always been filled from
SPDXIdentifier. A classifier passed by the caller is kept, because it is their
statement, and reported on the console.
339 f-strings that interpolate nothing lost their f prefix, across 33 modules.
An edge joining two components of a Graph moves the smaller component’s vertices into the
larger one. It moved the destination’s vertices into the source’s component whatever their sizes, so an edge from
a new vertex into a big component moved the whole component: building a graph of 10000 vertices from an edge
list took 633 ms, now 55 ms; 10000 calls of EdgeFromNewVertex() 1830 ms, now 39 ms.
So the component kept is the larger one, not always the source’s; of two of equal size, still the source’s. The
other one is dropped with its name and key-value pairs, as before.
⚠️ str() of a Program or an argument is a command line escaped for the
current platform. It put each argument in double quotes without escaping the quotes inside: the two arguments
-m and a "b" c came out as "-m" "a "b" c". Now they are -m 'a "b" c' for a POSIX shell and -m "a
\"b\" c" on Windows. repr() is a Python literal with strings in double quotes.
pyTooling.MetaClasses imports TAttr and
TAttributeFilter from pyTooling.Attributes. It declared its own copies, and
its filter was a union of attribute instances, while the predicate of GetMethodsWithAttributes takes
attribute classes.
⚠️ A secondary base-class built by ExtendedType with slots=True, but without
mixin=True, raises BaseClassIsNotAMixinError, which was declared but never
raised. One with fields raised BaseClassWithNonEmptySlotsError, one without fields
was accepted. A plain class with non-empty __slots__ still raises
BaseClassWithNonEmptySlotsError.
The four methods writing a pyTooling.Graph.GraphML element are abstract in its base-class
Base. They raised a bare NotImplementedError when called, so a derived
class missing one could be instantiated. An element that is never written as a single tag, or never with an
opening and a closing tag, says so in the NotImplementedError of the other methods, as
Subgraph already did.
ExtendedType checks that every base-class of a slotted class or mixin-class uses
__slots__ without walking the whole hierarchy, when each direct base-class is a slotted class or mixin-class
built by ExtendedType (or Generic): those passed the same check when they were created. A
class below a hierarchy of depth 6 computes its slots in 7.6 µs instead of 10.7 µs on Python 3.14.
ExtendedType finds out whether a class’ __new__ is the replacement an abstract
base-class put in place without raising and catching an AttributeError. It ran for every class; creating
one takes 0.4 µs less on Python 3.14, 0.3 µs on 3.13.
pyTooling.Decorators
Reading a @readonly property runs property’s own __get__. The
__get__ overloads telling a type checker the getter’s return type are declared under
TYPE_CHECKING only; at runtime, they put a Python-level method call into every read. A read
takes 41 ns instead of 189 ns on Python 3.14, 27 ns instead of 123 ns on 3.13; a property 14 ns.
ExtendedType scans the members of the base-classes for an implementation of an
inherited abstract method only if a base-class has abstract methods, and looks at a member’s marker only if its
name is one of them. It scanned every member of every base-class for every new class. Over pyVHDLModel’s 412
classes, the abstract-method check took 2.2 ms, now 0.95 ms.
ExtendedType compiles a textual annotation once per distinct text. It compiled
every string annotation of every class anew - under from __future__ import annotations that is every
annotation - although the same text recurs throughout a model (Nullable[str]). The code object is cached and
evaluated in the namespaces of each class being created, as before. Resolving a text seen before takes 1.0 µs
instead of 11.4 µs (Python 3.14), and creating a class with postponed annotations takes 26 to 59 % less time.
pyTooling.Filesystem
pyTooling.Attributes.ArgParse
pyTooling.Common
pyTooling.Configuration
⚠️ pyTooling.Configuration.Configuration takes only configFile. A configuration is the root node, so
it initializes Node as one itself: its root is itself, and it has no parent.
A configuration node’s length is its raw node’s length, not a copy taken at construction.
pyTooling.Decorators
pyTooling.Dependency
pyTooling.GenericPath
pyTooling.Graph
⚠️ The elements take their key-value-pairs from KeyValuePairsMixin instead of each
defining the same dictionary methods, so the key and value type parameters are gone.
⚠️ Iterating a Graph yields its keys, like every element with key-value-pairs.
⚠️ An edge or link takes its kind as a parameter before keyValuePairs: edgeKind / linkKind in
EdgeToVertex, EdgeFromVertex, LinkToVertex and LinkFromVertex, kind in the constructors of
Edge and Link.
pyTooling.Graph.GraphML
pyTooling.MetaClasses
⚠️ Annotating __weakref__ or __dict__ raises an ExtendedTypeError. Both
became slots silently: a __dict__ slot accepted any attribute on an instance again, and a __weakref__
slot is requested with weakref=True now.
ExtendedType binds an annotated class variable before
__init_subclass__() runs. A derived class overriding a class variable with an annotation was
invisible to that hook, which read the inherited value.
pyTooling.Packaging
DEFAULT_PY_VERSIONS is 3.11 to 3.15, the maintained CPython versions - 3.10 reaches
its end of life in October 2026, 3.15 was released in October 2026. A package not stating pythonVersions gets
python_requires >=3.11 and a classifier for each of them.
pyTooling.Platform
pyTooling.Stopwatch
pyTooling.TerminalUI
pyTooling.Testing
⚠️ ApplicationTestcase handed a test’s environment to the program as its whole
environment, so every caller wrote {**environ, ...}. The variables it names are set, None removes one,
the rest is inherited.
pyTooling.Tracing
pyTooling.Tree
⚠️ len() and iteration of a Node address its key-value-pairs; the new
ChildCount and Children address the children.
⚠️ A Node takes its key-value-pairs from
KeyValuePairsMixin, so its key and value type parameters are gone.
General
⚠️ Membership tests are named Contains***; Has*** checks a property or feature of an object.
⚠️ 32 exception classes carry the ***Error suffix; ***Exception is reserved for a package’s own base
exception, ToolingException.
The eighteen constructors in pyTooling.Attributes document their parameters. Each carried the same
sentence describing *args/**kwargs, which none of them takes.
The four documentation-coverage findings that were real are fixed: pyTooling.Tree.Node.__delitem__(),
both comparison operators of License, and abstract_new().
Overview is a new section, last in References and Reports: a page per schema showing its full source
with a copy button, and offering the file itself for download. The page includes the schema from the package,
so there is no second copy to drift.
A schema is also drawn, by the directive xmlschema-graph of
pyTooling.Sphinx - complex types as records, containment as labelled edges carrying
the cardinality, and a node for an enumeration.
This release history was written, covering every release back to v0.5.0.
Stopwatch’s __enter__ and __exit__ document each condition they raise a
StopwatchError for - __exit__ named one of three. __getitem__ documented a
KeyError, but raises an IndexError.
Two doc-strings didn’t parse as ReST: lazy’s usage list was folded into its
summary, and the Windows GetMemoryUsage of pyTooling.Process had an unexpectedly indented line.
A parameter’s description opens with Optional, exactly when the parameter has a default value: 25 parameters
with a default gained it - the __exit__ parameters of the context managers, dest of the ArgParse flags,
kind of BaseEdge - and 56 parameters without one lost it, among them parameters
accepting None. pyTooling.Common.StringEnum.Parse() named a default its value doesn’t have.
Warnings explains the kinds of warnings, raising, collecting, escalation, threads, and how warnings are
documented and designed. Its example raised Python’s built-in Warning - an exception, not pyTooling’s
Warning - and didn’t parse.
The doc-strings of pyTooling.Warning describe what happens:
Raise() hands a warning to the current collector of the calling thread
instead of walking the call-stack, a CriticalWarning must be collected, and an
escalated warning is collected before EscalatedWarningError is raised.
A function reporting a warning names it in its doc-string and says it’s dropped without a collector - in
pyTooling.Filesystem, pyTooling.Dependency and ExtendedType. Each
warning class states whether it’s critical.
Most tbd pages are written: the Attributes, Configuration, ArgParse, Cartesian, Program, Stopwatch and
filesystem pages, the Meta-Classes background page and ExtendedType’s reference,
and tutorials on attributes, the CLI abstraction and exception hierarchies.
The glossary has 122 terms instead of 47, and none of them is a placeholder.
Pages compare pyTooling with competing solutions: the pipeline model, tracing, warnings, the terminal UI,
ExtendedType, attributes, REST, licensing, configuration, dependencies, versioning, test annotations, the
stopwatch, the linked list and the CLI abstraction.
The Dependency page renders every extra’s table from the package index, with pyTooling.Sphinx’
dependency-table directive.