Repository navigation
v10.0.0
Note
This release accumulated 329 merged pull-requests, grouped by section and namespace; the linked pull-requests carry the detail, doc/News.rst the reader-facing version.
- ✨ New packages
pyTooling.ControlFlow,pyTooling.CallGraph,pyTooling.CI,pyTooling.REST,pyTooling.Documentation,pyTooling.DataModel,pyTooling.TimeoutandpyTooling.Graph.GraphViz; traces render as Gantt charts and export OTLP/JSON; SPDX license expressions, version expressions and epochs; a test report format with four names per item.
⚠️ 32 exceptions renamed to***Errorwithout aliases;Stopwatch.DurationandSpan.Durationaretimedeltas;InheritDocStringtakes one strategy; membership tests areContains***;ExtendedTypeenforces its slot and mixin rules; attribute scopes are enforced; versions follow PEP 440's spellings; class constants are upper case (e.g.HEADLINE). - 🔧
ExtendedTypecreates classes faster (26 to 59 % less time per class with postponed annotations) and@readonlyreads 4× faster; FreeBSD is supported; packages default to Python 3.11 to 3.15. - 🪳
ExtendedType's abstract-method and slot handling; the GraphML writer;LineSegment2D.LengthandAngleTo; calendar versions compare every part;KeyValueFlagandOptionalValuedFlagparse their values; configuration, URL and version-range corner cases. - 📖 Most
tbdpages are written: tutorials, the glossary, competing solutions; hierarchies drawn as trees. - 🧪 3461 testcases instead of 1326, run in parallel.
- ⚙️ The pipelines run on
pyTooling/Actions@r9, test Python 3.15 and PyPy 3.12, run their single jobs on Python 3.15, clean up artifacts withCleanupArtifacts.yml, and refuse to tag a package PyPI would reject.
Important
The Sphinx extension and everything about GitHub Actions (run model, workflow file model, trace reader, Sphinx domain gha, the pyTooling program) moved into pyTooling.Sphinx and pyTooling.GitHub before this release; dev's history was rewritten, so no pyTooling release contains them. Their pull-requests are listed at the end, marked as moved.
Since v9.0.0 the package grew from 303 to 511 classes in 52 to 71 modules, and the testsuite from 1326 to 3461 passing testcases.
✨ New Features
-
pyTooling.Attributes- An attribute class states its scope as class keyword argument (#523):
class Hook(Attribute, scope=AttributeScope.Method). A derived class inherits it; overriding_SCOPEin the class body still works.
- An attribute class states its scope as class keyword argument (#523):
-
pyTooling.Attributes.ArgParse-
splitFormatparses an option's value[<format>:]<file>(#429), e.g.--output=json:report.json. The formats are aStringEnum; a value naming none gets itsDEFAULT, an enumeration without one raises aValueErrorwith a note. A Windows drive or a colon deeper in a path stays part of the path. -
NamedAndValuedArgumentandNamedTupledArgumentare exported (#400), taking__all__ofpyTooling.Attributes.ArgParse.Argumentfrom 13 to 15 names.
-
-
pyTooling.CallGraphpyTooling.CallGraphdescribes a program's call graph (#530): aCallGraphofFunctions andCalls from caller to callee, so a recursion is a cycle. A function is defined in the described code, elsewhere, or a placeholder for unknown targets of an indirect call; it may refer to its control flow graph, a call to its call site. Execution counts and key-value pairs as inpyTooling.ControlFlow;FunctionCount,ContainsFunction,GetFunction,IterateFunctions,CallTo/CallFromandToGraph().
-
pyTooling.CI-
A service-independent model of CI pipelines (#438):
PipelineGroup→Pipeline→Workflow/Matrix→Job/MatrixJob/MatrixWorkflow→Step, with times (a group spans and iterates what it holds, #418), an OpenTelemetryOutcomeand dependencies between siblings (AddNeed,Needs,Dependents,Validate()).Workflow.ToGraph()converts to apyTooling.Graph. GitHub Actions and GitLab CI map onto it;CIErroris its base exception,QualifiedNameMixinnames elements as GitHub Actions does (#412), every element carries key-value-pairs (#446). -
Workflows, matrices, jobs and steps carry an ordered list of rules (#570): a
Ruleholds a condition as written (never evaluated), aRuleOutcome(GitLab's sixrules:whenvalues) and key-value-pairs for service-specific parts. The first matching rule decides; an element without rules runs, one whose rules all fail doesn't. A GitHubif:is one rule, GitLab'srules:/workflow:rulesthe list.RulesMixingivesRules,RuleCount,AddRule,IterateRules; a rule given to a second element raisesAlreadyInPipelineError. -
pyTooling.CIdoesn't loadpyTooling.REST(#622):JSONObjectis imported frompyTooling.RESTand no longer re-exported, so the pipeline model loads no REST client (urllib.request,http.client,ssl):import pyTooling.CItakes 39 ms instead of 61 ms (Python 3.15).
-
-
pyTooling.CLIAbstractionProgram._CopyParameterscopies every argument set on a program to another instance (#497), for deriving a configured variant. An argument the target doesn't declare or has set already raises aKeyError. The CLIAbstraction example uses it.
-
pyTooling.Common-
pyTooling.Common.StringEnumis aStrEnumthat parses itself (#427, #429):Parseconverts a string to its member, raisesTypeErrorfor a non-string andValueError(listing the accepted values) for an unknown value, and answersNoneor""with the aliasDEFAULT- or withNonewhere a value may be absent.MediaType,SpanKindandResultderive from it. -
pyTooling.Common.getTypeNamereturns an object's type name without its module (#592, #593): a class, function or method (built-ins included) its own name, any other object its class' name. Exception messages use it, theGot type '…'notes keepgetFullyQualifiedName; pyTooling's 23 uses of__class__.__name__call it now.
-
-
pyTooling.Configuration-
A configuration node answers the mapping and sequence protocols (#375) —
Dictionary.keys(),.values(),.items()and.get(), and a sequence node answerslen()and indexing. -
A configuration dictionary can be iterated by keys and by items (#366) —
IterateKeys(),IterateValues()andIterateItems()onDictionary, for YAML and JSON alike. Bare iteration still yields only the values (__iter__is untouched).
-
-
pyTooling.ControlFlowpyTooling.ControlFlowdescribes a function's control flow graph (#529): aControlFlowGraphofBasicBlocks andEdges with an entry, an exit and an optional unwind block. AnEdgeKindsays why control continues (fall-through, jump, exception, abnormal exit); blocks and edges carry an optional execution count and key-value pairs.BlockCount,ContainsBlock,GetBlock,IterateBlocks,EdgeTo/EdgeFrom;ToGraph()converts to apyTooling.Graph, whichpyTooling.Graph.GraphMLwrites.
-
pyTooling.DataModelpyTooling.DataModelholds the foundations of data models (#571, #572):KeyValuePairsMixinattaches key-value-pairs to an element, accessed like a dictionary (element[key],get(key, default)(#576),del,in,len, iteration over the keys). The root classes ofpyTooling.CI,pyTooling.ControlFlow,pyTooling.Graph(#575),pyTooling.Tracing(#576) andpyTooling.Tree.Node(#574) derive from it instead of each defining the same methods, and reject non-mapping key-value-pairs (TypeError).Predicate[T](#577),Callable[[T], bool], types element-selecting functions in 29 annotations across eight packages.
-
pyTooling.Dependency-
UnparsableURLWarning(#636): a project URL a package index publishes that isn't a URL is reported and skipped;UNKNOWNand empty values count as not published. -
New exception
PackageIndexError(DependencyError)(#583): the package index responded with an HTTP error status other than404 Not Found. It is caused by therequests.HTTPError. -
PackageDependencyGraph.ToGraph()converts a dependency graph into apyTooling.Graph(#498): a vertex per package version (ID: thePackageVersion; license, release time and URLs as typed key-value pairs) and an edge to every version it needs, so graph algorithms (topological order, cycle detection) and GraphViz/GraphML apply. The Package Dependencies page documents the format. -
The package-override file has a JSON schema (#389),
pyTooling/Resources/PackageOverrides-v0.1.schema.json, shipped in the wheel.LicenseOverrides.SCHEMA_FILESmaps a structure version to its file andSCHEMA_VERSION_LATESTnames the newest, likeReportWriter.SCHEMA_FILES/SCHEMA_VERSION_LATESTfor the test report XSD (bothdict[SemanticVersion, Path]). It rejects misspelled fields (licenseUrl), a bare date asanalysedAtand an unquotedversion; a file stating no packages is valid. Editors validate via# yaml-language-server: $schema=.... -
RequirementsFilereads a requirements file as a tree (#377):-rlines are followed, every file knows itsParent,RootandHierarchy.AllRequirementsyields the requirements in stated order, the nearer statement winning;AnalyzedRequirementFileslists the tree by resolved path.RequirementsFile.Parse(path)reads the files (#585); the initializer takes what a file states, for building a tree by hand (a file in two trees raisesAlreadyInRequirementsTreeError). A diamond reference is read once (#601), the second is aRequirementsFileReference; only a real cycle raisesCircularRequirementsFileError.AnalysisDuration(#595) isParse's time.
-
-
pyTooling.DocumentationpyTooling.Documentationis a new module (#340) holding the helpers that work on doc-strings.splitDocString()splits one into its summary and its body.
-
pyTooling.Exceptions-
ExceptionBase.Message(#584): read-only property returning the exception's message (args[0]). -
Exceptions carry the object they are about (#579, #594), e.g.
KeyNotFoundError.Key,CycleError.Path- about 35 exceptions across the packages (MetaClasses: new common baseInvalidBaseClassError). The new parameters are keyword-only, required where every raise knows the value (#634). Not-found errors are alsoLookupErrors (#604, #599),RequirementsFileNotFoundErroraFileNotFoundError,UnsupportedValueTypeErroraTypeError. Warnings carry their subject too (#598, #608, #609), e.g.ProjectName/Version;BrokenRequirementWarningis the base ofUnparsableRequirementWarningandUndeclaredExtraWarning;⚠️ PermissionWarningtakes a message and carries theDirectory. -
Common bases for the errors every data structure raises (#568, #616, #348, #644):
DuplicateElementError,AlreadyInHierarchyError,NotInSameHierarchyError,NotAChildError(#578),InUseError,ElementNotFoundError(aLookupError) andCycleError. A package's exception derives from its base exception and a common base, so one clause catches e.g. every duplicate; pyTooling's errors do (e.g.DuplicateVertexError,AlreadyInTreeError,Graph.CycleError), messages unchanged. Upstreamed from pyEDAA.Reports.
-
-
pyTooling.Filesystem-
File.ToTree()converts a file storage object to a tree node (#552), likeFilename.ToTree()andSymbolicLink.ToTree(): its value is the file object, withkindandsizeas key-value pairs. -
Root.Delete()deletes a scanned directory tree and breaks its reference cycles (#621): all directories, filenames, symbolic links and file objects are disconnected, also those aCollapse()removed. A droppedRootwas freed only by the cyclic garbage collector; afterDelete(), reference counting frees it at once (31 296 files: 19.1 MiB).
-
-
pyTooling.GenericPath- A path or a URL composes with a resource (#414):
path / resourceandURL / resource, taking a string or a path on the right, aspathlib.PurePathdoes. The right side is an RFC 3986 relative reference with its own query and fragment; an absolute path replaces, a trailing delimiter on the left is dropped first.WithoutTrailingDelimiterandWithoutTrailingSlashremove one. A string naming a scheme, an authority or a forbidden character is refused.
- A path or a URL composes with a resource (#414):
-
pyTooling.Graph- A graph's transitive edges (#443):
IterateTransitiveEdges()yields the edges a longer path implies (A → CbesideA → B → C),IterateTransitiveEdgesWithPath()each with that path,RemoveTransitiveEdges()removes them (transitive reduction),AnnotateTransitiveEdges()marks every edgeEdgeKind.DirectorEdgeKind.Transitive, optionally with the path as key-value-pair. Edges and links have a kind (BaseEdge.Kind,EdgeKind/LinkKind, set byedgeKind/linkKindorkind), possibly from a user enumeration, and it is a type parameter (#445):Edge[..., MyKind, ...]typesKindasMyKind. Graph.Delete()deletes all elements of a graph and breaks their reference cycles (#617): subgraphs, views, components, vertices, edges and links are disconnected and their key-value-pairs removed; the empty graph can be refilled, and reference counting frees it at once (100 000 vertices: 112.6 MiB, 217 ms instead of 345 ms forgc.collect()). A vertex's value is kept.
- A graph's transitive edges (#443):
-
pyTooling.Graph.GraphVizpyTooling.Graph.GraphVizwrites Graphviz' DOT language (#475), the counterpart ofpyTooling.Graph.GraphML:Node,Edge,DefaultAttributes,HTMLLabelandRecordLabelwith checked attributes,quote(), andGraph/Subgraphdocuments (#478, with #476, #477).FromGraph()andFromTree()convert apyTooling.GraphorpyTooling.Tree; a derived class overrides_ConvertVertex()and siblings.BaseGraph.ContainsNode()asks for a node by identifier (#484).
-
pyTooling.Licensing-
28 more SPDX licenses (#337, #388, #430, #483), taking
SPDX_INDEXfrom 4 to 32 — closes #15. Among them0BSD,MIT-0, the six Creative Commons 4.0 licenses (CC-BY-4.0…CC-BY-NC-ND-4.0; no PyPI classifier, so asking for one raisesValueError) and Pillow'sMIT-CMU(OSI approved since 2024-09-20). ALicenseis hashable and equals its SPDX identifier string. -
The GNU licenses' texts in all formats gnu.org publishes (#486):
LICENSE_TEXT_URLSgainstxt,mdandtexfor GPL-2.0, LGPL-2.1, LGPL-3.0 (new entry) and AGPL-3.0,rstfor GPL-3.0;LICENSE_URLSgains AGPL-3.0's page. -
pyTooling.Licensingmodels an SPDX license expression as a tree (#367), e.g.Apache-2.0 OR MITorGPL-2.0-only WITH Classpath-exception-2.0.LicenseExpressionis the base ofSPDXLicense,AndOperator,OrOperator,WithOperatorandOrLaterOperator(+suffix). Each node knows itsParentandRoot; assigningParentre-roots the subtree.IterateExpression()walks depth-first in infix order. A malformed expression raisesLicenseExpressionError.
-
-
pyTooling.LinkedListLinkedListgains five specific exceptions andGraphthree (#322): 20 raise sites name what went wrong instead of raising the module's base, from which every new class derives.
-
pyTooling.MetaClasses-
ThisClassis a sentinel for a class variable holding its own class (#410), which doesn't exist yet while its body runs. -
A slotted class can be referenced weakly (#336) with the
weakrefclass keyword argument — closes #49.weakref=Trueadds__weakref__to the slots like any field: derived classes inherit it, a mixin-class passes it on. -
A class or a mixin can name the members it expects (#329), with the
expectsclass keyword argument: checked at class construction, reported on instantiation, like an abstract class. An override expects only what its own marker names (#533), a class fulfilling an expectation its base missed can be instantiated with parameters (#532), and the search for marked methods skipsobject(#536) and classes, so a class variable holding a class with expectations keeps it (#626). -
ExtendedType'sexpectsaccepts atyping.Protocolclass (#611), besides an iterable of names: the protocol's methods, properties and annotated fields, inherited ones included, sorted by name. A non-protocol raisesTypeError,typing.ProtocolitselfValueError. Annotatingselfwith the protocol lets a type-checker check the mixin's calls.
-
-
pyTooling.Packaging-
⚠️ A package describes itself once (#340): its short description is the first paragraph of its module doc-string, folded to one line. Withoutdescriptionand doc-string,DescribePythonPackageraises aPackagingError;descriptionmoved behind the required parameters. -
A package can declare any entry point group (#333) —
consoleScripts,guiScriptsandpytestPlugins; onlyconsole_scriptswas reachable. -
pyTooling.Packagingraises its ownPackagingError(#340), so a caller can catch what this module reports. -
⚠️ Direct references are left out of the extras (#561): PyPI rejects metadata with a requirement likepyTooling.Sphinx @ git+https://…@devwith a bare400 Bad Request.DescribePythonPackageremoves them from every extra and reports each as a criticalDirectReferenceWarning; base requirements are kept. pyTooling's ownsetup.pycollects and prints them.
-
-
pyTooling.RESTpyTooling.RESTis a new package: a client for JSON REST APIs (#413) on the standard library. A resource is addressed by its path below the base URL;GetJSONObjectreads one and its next page, four write methods change one. Transiently failing idempotent requests are retried with exponential backoff.MediaType.Matcheschecks aContent-Typeheader, RFC 6839 suffix included.
-
pyTooling.TerminalUI-
TerminalApplication.WriteErrorNotewrites an error's note (#424) - the advice for fixing it;Errorwas the only severity without one. -
TerminalApplicationProtocol(#611): the 22 members a command-handler mixin-class can expect from aTerminalApplication(e.g.Width,Exit,Write***), with signatures and doc-strings. Withexpects=TerminalApplicationProtocol, mixing it into anything else fails on instantiation.
-
-
pyTooling.Testing-
Testcaseprovides all eight assertions Python 3.14 added (#653) on Python 3.11 to 3.13 - besidesassertHasAttr/assertNotHasAttralsoassertIsSubclass,assertNotIsSubclass,assertStartsWith,assertNotStartsWith,assertEndsWithandassertNotEndsWith- with Python 3.14's messages. -
A test run produces a report that can hold what JUnit cannot (#331, #332, #334, #335).
@testsuiteand@testcasemark what a runner collects; each item has four names — ID, title, summary, description, the last two falling back to the doc-string. Two pytest plugins:pyTooling.Testing.PyTestcollects the markers (a marked method of aunittest.TestCaseclass under its own name, #480),pyTooling.Testing.ReportWriterwrites our own XML format (--pytooling-xml=report/unit/TestReport.xml) alongside--junit-xml.Test suites nest instead of being flattened into a dotted
classname, every level can carry prose, line breaks survive. The format has a versioned schema,TestReport-v0.1.xsd, shipped as a package resource. The names also reach the JUnit file as test suite properties keyed by the level's dotted path. Markers aren't collected as testcases (#341); a summary over 200 characters is left out with aPytestCollectionWarning(#519).
-
-
pyTooling.Timeout- New package
pyTooling.Timeout: a cooperative timeout for the current thread (#600, #602, #603).with Timeout(20.0):sets the thread's current timeout; code below asksTimeout.Remaining()for the seconds left, and a loop callsTimeout.RaiseIfExpired(message)to abort with aTimeoutExpiredError(also aTimeoutError) carryingSecondsandOverrun.Timeout.Current()returns the timeout; a nested one can only shorten the time left. Nothing interrupts a thread: the limit acts where code checks it. The dependency resolver honours it (#606). Time is measured withtime.perf_counter_ns(), as inStopwatchandTracing.
- New package
-
pyTooling.Tracing-
A span or trace left by an exception records it (#514) in
Span.Exception; the exception propagates as before, and the OTLP/JSON export reports the span withstatusSTATUS_CODE_ERRORand the exception's type and message (OTLPStatus). -
A trace renders as a Gantt chart (#404).
pyTooling.Tracing.Render.GanttLayoutarranges a trace's timespans in rows independently of a drawing library - a job's waiting time as a light bar in front of it, filters for CI traces (ciSpanFilter,StepExclusion), a category per runner image including the MSYS2 environment, per-category waiting and running statistics. TheRendererbase-class holds colors, title and legend;MatplotlibRendererdraws SVG, PNG or PDF. New extrapyTooling[diagram]installs matplotlib and plotly.The layout derives from
pyTooling.Diagram.Gantt(#421), a new namespace for diagram data models: aDiagramofRows ofBars on a stated origin; a bar answers its position as times and as distances from the diagram's origin and from its row. -
A Gantt chart written as SVG is collapsible (#405):
MatplotlibRenderer(layout, collapsible=True)embeds a script, so a click on a called workflow or job hides or shows the rows below it;collapsedKinds(default: jobs) start collapsed. Script and stylesheet are resource files in the new packagepyTooling.Resources.Tracing, embedded so the file stays self-contained. A collapsible chart can't be written as PNG or PDF. -
A Gantt chart as an interactive HTML page (#406):
pyTooling.Tracing.Render.Plotly.PlotlyRendererdraws the same rows with plotly - zoomable, pannable, a hover text per bar with begin, end and duration, and a legend toggling categories; the time axis starts at the trace's begin.Write()writes HTML or plotly's JSON;PlotlyJavaScriptembeds plotly's library (default, works offline) or loads it from the CDN. Both renderers check every constructor parameter (None, type, range);QUEUED_LEGEND_LABELnames the waiting bars. -
A trace's elements share one base-class,
TraceElement(#422): aSpanand anEventhave a name, a parent and attributes, validated in one place.get(key, default=None)reads an optional attribute - since #576 the mixin's, withIterateAttributes()yielding(key, value)pairs. -
pyTooling.Tracing.CIreads a CI pipeline into a trace (#403):PipelineTrace,WorkflowSpan,MatrixSpan,QueuedSpan,JobSpan,StepSpan, each naming its kind and taking the OpenTelemetry CI/CD attributes as parameters. They are service-independent; pyTooling.GitHub's reader of GitHub Actions runs is the first reader. -
A trace can be built from times recorded elsewhere (#402), e.g. a CI job or a log line; a
Spantook its times only from awith-statement. -
A software execution trace exports itself as OTLP/JSON (#339):
Trace.ToOTLPJSON(),ToOTLPJSONString()andWriteOTLPJSONFile(), accepted natively by an OpenTelemetry collector and by Jaeger since v1.35. The document is typed (twelveTypedDictclasses for the OTLP messages); identifiers are drawn when aTraceorSpanis constructed, so two exports report the sametraceIdandTrace.TraceIDcan be handed to another process. Timestamps are exact to the microsecond (#515). -
A trace reads itself back from OTLP/JSON (#348):
Trace.FromOTLPJSON(),FromOTLPJSONString()andReadOTLPJSONFile(). The tree is reassembled from the spans'parentSpanIdreferences without recursion; re-exporting reproduces the document byte for byte. A document is first validated against the new JSON schemaOTLP-Traces-v1.0.schema.json(TraceCollection.SCHEMA_FILES,SCHEMA_VERSION_LATEST) (violations become notes of theTracingError, naming their position) which needs the new extrapyTooling[tracing](jsonschema); writing needs none. -
A
TraceCollectionholds what several documents carry (#348): complete traces and fragments - timespans whose parent arrived in no document yet. A fragment's root is aFragment, keeping the identifiers of its trace and awaited parent. Documents may be added in any order (AddOTLPJSON(),AddOTLPJSONString(),AddOTLPJSONFile()); a fragment is attached once its parent arrives, a colliding document changes nothing. Traces and timespans are indexed by identifier (collection[identifier],in,TraceIDOfSpan(),IterateFragmentsOf()); the collection writes itself back as OTLP/JSON. Errors:TraceNotFoundError,SpanNotFoundError,DuplicateTraceError,DuplicateSpanError,SpanCycleError. -
Writing a file into a missing directory raises a
DirectoryCreatedWarning(#348) before the directory is created, so a program can report the path.
-
-
pyTooling.Tree-
Node.ChildrenandNode.ChildCount(#573): a tuple of the direct children and their number; the Tree page namedChildrenalready. -
Node.RemoveChildandNode.RemoveChildren(#578), counterparts ofAddChild/AddChildren: a removed child takes its subtree along and becomes a root, like assigningNoneto itsParent. A node that isn't a direct child raises the newNotAChildError, carrying the node inChild, derived from the common basepyTooling.Exceptions.NotAChildError. -
Node.Delete()deletes a node and its subtree and breaks their reference cycles (#620): a node with a parent is removed first, likeRemoveChild(); then every node of the subtree loses root, parent, children and key-value-pairs and is unusable - its value is kept; reference counting frees it at once (100 000 nodes: 40.3 MiB in 19.5 ms instead of 52 ms forgc.collect()).
-
-
pyTooling.URI-
New package
pyTooling.URI(#662): URLs, their hosts, schemes and query parameters, and the SCP-like address - formerlypyTooling.GenericPath.URL. -
URIis the generic URI of RFC 3986 (#663) -Scheme,SchemeSpecificPart,Query,Fragment- for every scheme (mailto:,tel:,urn:);URLderives from it.URI.Parsereturns aURLfor a URI with//and a scheme ofProtocols.URL.Parse(text, defaultScheme=Protocols.HTTPS)prepends a scheme to a URL without one, as a browser does. -
URNis a Uniform Resource Name of RFC 8141 (#664), derived fromURI:NamespaceID,NamespaceSpecificString,Resolution(?+); the q-component (?=) is read intoQuery, the f-component intoFragment.URN.Parsechecks the namespace identifier and the characters;URI.Parsereturns aURNfor the schemeurn-urn:isbn:0451450523had been a URI with the scheme-specific partisbn:0451450523. -
SCPAddressreads[user@]host:[path](#661), as scp and Git write it (git@github.com:org/repo.git);ToURL()givesssh://git@github.com/~/org/repo.git. -
A query parameter is an object (#660):
QueryParameterwithKey,Value,Values;KeyValuePairfor a key written once,KeySequencePairfor a repeated key (Value= last value). -
URL.Parse(url, iri=True)reads IRIs (#639, RFC 3987): a Unicode host becomes its IDNA 2008 / UTS #46 A-label (www.müller.de→www.xn--mller-kva.de), other non-ASCII characters are percent-encoded;Host.UnicodeHostnamegives the Unicode form. New extraidna(pyTooling[idna]).
-
-
pyTooling.Versioning-
⚠️ PythonVersionunderstands the spellings PEP 440 normalizes, and keeps them (#436, #517):10.0.0-pre1,-preview1andc1are release candidates,10.0.0-dev,.devanddevare.dev0. A parsed version is written as parsed, compares and hashes like its normal form;Normalize()returns that form,Parse(..., normalize=True)returns it directly.SemanticVersionkeeps the release level's spelling too (spellingparameter,ReleaseLevelSpelling). Since #517 an own case-insensitive pattern reads every PEP 440 spelling - see Bug Fixes. -
Calendar versions carry a release level, a post-release, a development release, a build number and a postfix (#640, #649), as
SemanticVersiondoes:CalendarVersion,YearMonthVersion,YearWeekVersion,YearReleaseVersionandYearMonthDayVersionparse, compare, hash and write e.g.2024.01-rc1,2024.01.post2.dev3,2024.10.15+deadbeefand OSVVM's2025.06a. Separators are permissive (.,-or none); a letter without a number is a postfix, so2025.06afollows2025.06, and the postfix separator is kept (#649):2025.06ais written2025.6a. New keyword-only parameterslevel,number,post,dev,hashandpostfixSeparator;format()knows%p,%b,%r,%R,%n,%dand%P. Parsing a numbers-only calendar version is 5-13 % slower. -
pyTooling.Versioningparses and matches version expressions (#369) — what a requirement file writes, in four dialects (e.g. npm's^1.2.3, dpkg's>>1.0, <<2.0).VersionConstraintis one comparison and a container of the versions satisfying it (in).VersionComparisonnames the ten comparisons;RangeVersionConstraintis the base of the three shorthands deriving an upper bound:CompatibleVersionConstraint(PEP 440's~=),CaretVersionConstraintandTildeVersionConstraint(npm's^and~). AVersionExpressionis a conjunction of constraints (an empty one matches every version), the ecosystem-neutral base ofPythonVersionExpression,NPMVersionExpressionandDebianVersionExpression. Both convert withToVersionRange()where the comparison describes a single interval. -
⚠️ A version number can carry an epoch (#370), for a project whose new version numbers sort below the old ones:SemanticVersion.Parse("2:1.0.0")(Debian's spelling, the default),PythonVersion.Parse("2!1.0.0")(PEP 440's). A scheme spelling the separator differently overrides_EPOCH_SEPARATOR(ClassVar[str], #374).Version.Epochreads it,Parts.Epochsays it is present, and an epoch orders before every other part — see Breaking Changes for the renumbering. -
A
VersionRangecan be open at either end (#371): either bound may beNone(unbounded), so>=1.0and Maven's[1.0,)are expressible andVersionConstraint.ToVersionRange()works one-sided.__and__and both setters handle it; two bounds still need compatible version types.
-
-
pyTooling.WarningDirectoryCreatedWarning(#348): a non-critical warning carrying theDirectorya writer is about to create;pyTooling.Tracing's OTLP/JSON writers raise it.
⚠️ Breaking Changes
-
pyTooling.Attributes-
⚠️ In a class not built byExtendedType,GetFunctions()no longer returns marked methods (#499);GetMethods()does. -
⚠️ An attribute's scope is enforced (#489): applying an attribute where itsAttributeScopedisallows raises the newAttributeScopeError; pyTooling's own attributes keep the default scopeAny.
-
-
pyTooling.Attributes.ArgParse-
⚠️ TerminalApplication._PrintHelpmoved toArgParseHelperMixin, which owns the parsers it prints (#329). An application calling_PrintHelp()derives from both classes. -
⚠️ AnOptionalValuedFlagpasses a string orNoneinstead of a boolean (#673) - see Bug Fixes. A bare option passesdefaultValue,""by default, which is false: askargs.x is not Nonewhether the option was given.
-
-
pyTooling.CLIAbstraction-
⚠️ A program found inPATHis the full path (#495) inProgram.Pathand as the first item ofToArgumentList(), e.g./usr/bin/gitinstead ofgit. -
⚠️ str()of aProgramor an argument is a command line escaped for the platform (#496), instead of every argument in double quotes; a key-value flag writes one pair per key. Code parsing that text adapts;ToArgumentList()returns the list. -
⚠️ Program._executableNamesis_EXECUTABLE_NAMES(#613): class constants are upper case; a class still setting_executableNamesis no longer read. There is no alias.
-
-
pyTooling.Configuration⚠️ pyTooling.Configuration.Configuration.__init__no longer takesrootandparent(#368) — see Changes. A format implementation drops both arguments.
-
pyTooling.Decorators⚠️ InheritDocStringlosesmerge,summaryOnlyandorder;DocStringMergeOrderis renamed toDocStringMergeStrategy(#327),DerivedFirsttoBaseLast, without aliases. The default changes: on an entity with a doc-string,@InheritDocString(Base)appends the base's instead of replacing it.merge=TrueisDocStringMergeStrategy.BaseFirst: a one-substitution migration.
-
pyTooling.Dependency-
⚠️ The extrapypiis nameddependencies(#635): installpyTooling[dependencies]; there is no alias. -
⚠️ A release version the parser can't read raises a criticalUnsupportedVersionWarning(#581) instead of aprint(), e.g. pytz' pre-PEP 4402004d…2013d. The release is still skipped; the warning carriesProjectNameandVersion. Without aWarningCollector,Project.UpdateDetailsFromPyPIJSON,Project.DownloadDetailsandPythonPackageIndex.DownloadProjectraiseUnhandledCriticalWarningError; call them insidewith WarningCollector() as warnings:. -
⚠️ BrokenRequirementWarningandReleaseDetailsWarningare critical warnings (#564): without aWarningCollector,RequirementsFile.Parse,Project.DownloadReleaseDetails,PythonPackageIndex.DownloadProject(..., LazyLoaderState.PostProcessed)and theResolverraiseUnhandledCriticalWarningError(the warning is its__cause__) instead of dropping it. Wrap such a call in a collector.
-
-
pyTooling.Exceptions⚠️ ExceptionBasehas nomessagefield anymore (#584): usestr(ex)or the new read-onlyExceptionBase.Message. pyEDAA.OutputFilter'sClassificationExceptionread it; fixed by edaa-org/pyEDAA.OutputFilter#118.
-
pyTooling.Filesystem-
⚠️ Directory.ScanDuration,AggregateDurationandDocker.LayerCake.SlicingDurationreturn atimedelta(#595) instead of afloatof seconds. Call.total_seconds()for the number. -
⚠️ PermissionWarningtakes a message and carries the directory (#609):PermissionWarning(path)becomesPermissionWarning(message, directory=...), andwarning.Pathbecomeswarning.Directory.Path.
-
-
pyTooling.GenericPath-
⚠️ PathMixin.Parse(path, root=None)reads the flavour'sELEMENT_TYPE(#414) instead of taking the path and element classes: a path flavour outside pyTooling drops both arguments and declaresELEMENT_TYPE. -
⚠️ The four mixin-classes ofpyTooling.GenericPathare spelled***Mixin(#419), without aliases:PathMixIn,ElementMixIn,RootMixIn,SystemMixIn→PathMixin,ElementMixin,RootMixin,SystemMixin. No sibling repository imports them.
-
-
pyTooling.Graph-
⚠️ The elements take their key-value-pairs frompyTooling.DataModel.KeyValuePairsMixin; the key and value type parameters are removed (#575):Graphtakes 11 type parameters instead of 23,VertexandSubgraph11 instead of 17,ViewandComponent11 instead of 19,EdgeandLink4 instead of 5 (the kinds are type parameters now, #445). The old number raises aTypeError, also at import for an annotated class field. The 16***DictKeyType/***DictValueTypetype variables are removed; non-mapping key-value-pairs raise aTypeError. -
⚠️ Iterating aGraphyields its keys (#575), aslen(graph)counts its key-value-pairs; it yielded ID-less vertices, then the other vertices' IDs.IterateVertices()yields the vertices. -
⚠️ pyTooling.Graph: the kind precedeskeyValuePairs(#443) inEdgeToVertex,EdgeFromVertex,LinkToVertex,LinkFromVertexand the constructors ofEdgeandLink: a key-value mapping passed positionally lands in the kind. PasskeyValuePairsby keyword.
-
-
pyTooling.MetaClasses-
⚠️ slots=Trueon a class whose base-class has no slots raisesBaseClassWithoutSlotsError(#638): declare the base withslots=True, or dropslots=Truefrom the derived class. No class of the family is affected. -
⚠️ A mixin-class' slot that exists already raisesDuplicateFieldInSlotsError(#504): a field of the class itself, a slot of the primary inheritance line, or a slot of a second mixin-class - see Bug Fixes. -
⚠️ A slotted secondary base-class has to be a mixin-class, also without fields (#505), elseExtendedTyperaisesBaseClassIsNotAMixinError: declare it withmixin=Trueor apply@mixin. No consumer in the workspace uses such a base-class. -
⚠️ A class variable shadowing a mixin-class' slot raisesDuplicateFieldInSlotsError(#373) at class creation - see Bug Fixes.ClassVar[...]is not the fix: rename the class variable, or drop the slot. -
⚠️ Annotating__weakref__or__dict__raises anExtendedTypeError(#336) — see Changes. Passweakref=Trueinstead of annotating__weakref__; a class whose instances need a__dict__usesslots=False.
-
-
pyTooling.Packaging-
⚠️ DescribePythonPackageraises when it finds no description (#340), instead of publishing a package without one: passdescriptionor give the file named bysourceFileWithVersiona module doc-string.descriptionmoved behind the required parameters; pass it by name. -
⚠️ A direct reference in an extra raisesUnhandledCriticalWarningError, unless aWarningCollectorcollects it (#561): asetup.pywhose extras may contain one wrapssetup(...)inwith WarningCollector() as warnings:. Known: pyTooling.Sphinx, pyTooling.GitHub, pyEDAA.Reports. -
⚠️ A package described withDescribePythonPackagecarries noLicense ::classifier (#338); its license is the SPDX expression inlicenseonly. A consumer reading the license from the classifiers readslicenseinstead.
-
-
pyTooling.Platform -
pyTooling.Stopwatch⚠️ Stopwatch.Duration,ActivityandInactivityreturn atimedelta(#595) instead of afloatof seconds. Use the newDurationInSeconds,ActivityInSecondsandInactivityInSecondsfor the number.
-
pyTooling.TerminalUI-
⚠️ An application names its package once, asDUNDER_MODULE(#637):ISSUE_TRACKER_URLis removed and_PrintVersion()takes no module - setDUNDER_MODULE = myPackagein the application class. -
⚠️ TerminalBaseApplication.PrintMissingDependencyException()isPrintMissingDependencyError()(aa18469), after the exception it prints. There is no alias. -
⚠️ TerminalApplication.HeadLineisHEADLINE,TerminalBaseApplication.ForegroundisFOREGROUND(#613): the old names are no longer read, and**self.Foregroundfails. There are no aliases.
-
-
pyTooling.Testing-
⚠️ ApplicationTestcase.RunEntrypointandRunModulemergeenvironmentinto this process's environment (#492) instead of replacing it; a value ofNoneremoves a variable. -
⚠️ ApplicationTestcase._consoleScriptand_runnableModuleare_CONSOLE_SCRIPTand_RUNNABLE_MODULE(#613): the old names are no longer read. There are no aliases.
-
-
pyTooling.Tracing-
⚠️ Span.Durationreturns atimedelta(#605) instead of afloatof seconds; arithmetic with a number or a float format spec raises aTypeError. UseSpan.DurationInSecondsfor the number. -
⚠️ Iterating aTrace,SpanorEventyields the attribute keys (#576), as for every key-value element;IterateAttributes()yields the former(key, value)pairs. Keys areHashable, valuesAny; the OTLP export still checks each value.
-
-
pyTooling.Tree-
⚠️ len()and iteration of aNodeaddress its key-value-pairs (#573), like[],delandin:len(node)counts the pairs, iteration yields the keys; both addressed the children. UseChildCount,ChildrenorGetChildren(). -
⚠️ Nodetakes two type parameters instead of four (#574):Node[IDType, ValueType]; the type variablesDictKeyTypeandDictValueTypeare removed.Node[a, b, c, d]raises aTypeError, also at import for a module-level alias (pyTooling/pyTooling.Sphinx#51). Key-value-pairs given as anything but a mapping raise aTypeError.
-
-
pyTooling.URI-
⚠️ pyTooling.GenericPath.URLispyTooling.URI(#662): importURL,Host,Protocols,URLError, ... frompyTooling.URI; there is no alias. -
⚠️ URL(...)requires a scheme (#663):NoneraisesValueError. Pass aProtocolsmember. -
⚠️ URL.Queryholds aQueryParameterper key (#660), not a string: readurl.Query[key].Value. The constructor'squeryis an iterable of parameters, not a mapping. -
⚠️ URL.Parserejects strings that aren't URLs (#636):scheme://and a host are required (onlyfile/unixmay omit the host);"","nonsense",host:portand opaque URIs (mailto:,sip:) raiseURLError.URL_REGEXPrequires the scheme and ends in\Z. -
⚠️ Hostrejects a host name that isNoneor empty with aValueError(#485). An empty name was accepted, andNoneraised aTypeError.
-
-
pyTooling.Versioning-
⚠️ The third numeric part of aYearMonthVersion,YearWeekVersionorYearReleaseVersionis its build number (#640):YearMonthVersion.Parse("2024.10.15")is build 15 of2024.10instead of aValueError. To reject a date, parse withYearMonthDayVersionor checkParts.Build in version.Parts. -
⚠️ A calendar version compares and hashes every part (#640) — see Bug Fixes:YearMonthVersion.Parse("2025.06a") == YearMonthVersion.Parse("2025.06")isFalse, and a build number counts. To compare only the numbers, compareYearandMonth(Week,Release,Day). -
⚠️ A postfix makes a version newer (#640) — see Bug Fixes:1.2.3 < 1.2.3+build.5. To sort both alike, remove the postfix first. -
⚠️ SemanticVersion.Parse("v2.0.0-rc.1")is release candidate 1 (#640) — see Bug Fixes. Code reading the release number fromPostfixreadsReleaseNumber. -
⚠️ SemanticVersion.Parse("1.0a1")is written1.0.a1(#436), as parsed, not1.0.alpha1. A version constructed without a spelling is written as before. -
⚠️ A dev or post part counts when it's present (#436):1.0.dev0 == 1.0and1.0.post0 == 1.0areFalsenow, as PEP 440 orders them. -
⚠️ SemanticVersion.Parse("1.2.3foo")raisesValueError(#672) instead of returning1.2with the postfix3foo; so do1.2.3_xand a fifth number (1.2.3.4.5) - see Bug Fixes. Separate a postfix by.,-or+(1.2.3.foo,1.2.3-1). -
⚠️ PythonVersion.Parserejects what PEP 440 doesn't spell (#517): a local version after a.(1.0.0.fooraisesValueError; write1.0.0+foo, after-it still works), and the release levelsfinalandpl. -
⚠️ A release level without a number is a pre-release (#494):SemanticVersion.Parse("1.0.0-alpha")was a final release with the postfixalpha; it is1.0.0.alpha0. -
⚠️ Every member ofpyTooling.Versioning.Partshas a new value (#370):Epoch = 1takes the lowest bit, the other fifteen doubled (Major1→2,Prefix128→256,Hash512→1024). Use by name is unaffected; a persisted value or a comparison against a literal has to be re-read. -
⚠️ VersionRange.__and__no longer includes bounds the operands exclude (#372) — see Bug Fixes.
-
-
General
-
⚠️ Exceptions and warnings require their descriptive values (#634, #668); a message only is aTypeError, pass the values by keyword. Optional in v9.0.0:DuplicateVertexError(vertexID),DuplicateEdgeError(edgeID),SupervisedThreadError(threadName),VersionValidatorError(version). Without an initializer of their own in v9.0.0:KeyNotFoundError(key),UnsupportedValueTypeError,InterpolationError,PathExpressionError;NoSessionAvailableError,ProjectNotFoundError,ReleaseNotFoundError(projectName),ReleaseDetailsWarning; the slot, mixin, metaclass and abstract-class errors ofMetaClassesandUnannotatedFieldWarning;UnknownPlatformError(unknownValue);AlreadyInTreeError,NotInSameTreeError(otherNode). -
⚠️ Membership tests are namedContains***(#484), without aliases:Has***checks a property or feature,Contains***asks for a key, name or ID.Graph.HasVertexByID/HasVertexByValue→ContainsVertexByID/ContainsVertexByValue,GraphMLDocument.HasKey→ContainsKey. Vertex methods asking for an edge or link keepHas***. -
⚠️ 32 exception classes in 15 modules are renamed to the***Errorsuffix, without aliases (#320); v9.0.0 has only the***Exceptionnames.***Exceptionis reserved for a package's base exception (pyTooling:ToolingException, unchanged), as in PEP 8 and the builtins.EnvironmentExceptionbecomesEnvironmentVariableError, notEnvironmentError(a builtin alias ofOSError).
-
🔧 Changes
-
pyTooling.Attributes-
⚠️ An attribute files a method under methods when the decorator runs (#499), recognized by its qualified name, instead ofExtendedTypemoving it there after searching every module-level function the attribute marks: 100 classes of 50 marked methods beside 10000 such functions take 16-29 ms instead of 531-553 ms.GetMethods(scope=...)still needsExtendedType. -
⚠️ An attribute's scope is enforced (#489) — closes #384. A function in a class body is recognized as a method by its qualified name (<Class>.<name>), so all three kinds are checked when the decorator runs, for any class.Attribute.Scopeis an instance property; on the class it returned the property object. -
Attribute._SCOPE(#613) is the class constant's name; it was_scope.
-
-
pyTooling.Attributes.ArgParse⚠️ _PrintHelpis a method ofArgParseHelperMixin(#329), which owns the parsers it prints; it writes throughTerminalApplication'sWrite***methods.
-
pyTooling.Cartesian2D,pyTooling.Cartesian2D.Shapes,pyTooling.Cartesian3D- The type checks quote their types (#659), as the other packages do: "Parameter 'x' is not of type 'float' or 'int'." (was "integer or float"), "... 'Offset2D' or 'tuple'.", "... 'Point2D'.".
-
pyTooling.CLIAbstraction-
⚠️ str()of aProgramor an argument is a command line escaped for the platform (#496):shlex.joinfor POSIX,subprocess.list2cmdlineon Windows (pyTooling.CLIAbstraction.Argument.formatCommandLine). Every argument was put in double quotes, inner quotes unescaped, and a key-value flag's list in one pair of quotes; it is-Dkey1=value1 -Dkey2=value2now.repr()is a Python literal; dry-run mode logs the escaped command line. -
⚠️ Class constants are spelled in upper case (#613):Program._EXECUTABLE_NAMES, and the argument classes'_PATTERN,_NAME,_VALUE_PATTERN,_FALSE_PATTERNand_PATTERN_WITH_VALUE(were_executableNames,_pattern,_name, ...).
-
-
pyTooling.Common -
pyTooling.Configuration-
A configuration node's length is its raw node's length (#548), not a copy taken at construction - in preparation for writable configurations.
-
⚠️ pyTooling.Configuration.Configuration.__init__takes onlyconfigFile(#368): a configuration is the root node, its own root, without parent.pyTooling.Configuration.JSONandpyTooling.Configuration.YAMLalready passed onlyconfigFile.
-
-
pyTooling.Decorators-
Importing
pyTooling.Decoratorsdoesn't loadinspect(#624):InheritDocString's decorator function importsinspect.cleandocwhen it runs. -
⚠️ InheritDocStringtakes onestrategy, aDocStringMergeStrategy, instead ofmerge,summaryOnlyandorder(#327); its defaultBaseLastappends the base-class' doc-string below the entity's own.
-
-
pyTooling.Dependency-
⚠️ The extrapypiis nameddependencies(#635), by what it serves:MissingDependencyErrorhints and the docs namepyTooling[dependencies]. -
⚠️ A release version the parser can't read is reported as a criticalUnsupportedVersionWarning(#581) instead of byprint(). -
⚠️ BrokenRequirementWarning(withUnparsableRequirementWarningandUndeclaredExtraWarning) andReleaseDetailsWarningderive fromCriticalWarning(#564): a dropped requirement or release went unnoticed without a collector.UnknownLicenseWarning,UnresolvedRequirementWarningandUndeclaredExtraMarkerWarningstay non-critical. Without a collector,Project.DownloadReleaseDetailsraises for the first unreadable release after all downloads finished, dropping none. -
PythonPackageIndex.DownloadProjectlooks up a project once (#597) and returns it for every later call, also under another spelling (PEP 503 canonical name); a project cached at a lower load state is loaded further. A404 Not Foundis remembered (UnknownProjects) and raisesProjectNotFoundErroragain without a request. -
New module
pyTooling.Dependency.Resolver(#598, #599, #606):Resolver(index, *, strategy, limit, preReleases)picks the newest release matching each requirement and follows its requirements level by level, in parallel over oneaiohttpsession;ResolutionStrategy(Lazy/Eager),limitandPreReleasePolicy(default: pip's rule) steer it. EachResolvedRequirementhas aStatus(ResolutionStatus); an unknown project gives the newUnresolvedRequirementWarning, a failed request the new criticalPackageIndexFailureWarning; neither raises. No backtracking. Markers are evaluated againstenvironment(CurrentEnvironmentResolver: the running interpreter's); extras add their requirements. Inputs:ResolveRequirements(),ResolveRequirementsFile(),ResolvePackages()(an undeclared extra raises the newExtraNotFoundError). The currentTimeoutbounds a resolution (#606): the unresolved rest getsTimeoutExpiredand a criticalResolutionTimeoutWarning; the partial result is returned.
-
-
pyTooling.Exceptions-
⚠️ ExceptionBasehands its message toException.__init__(#584):argsis(message, )(was()),repr()shows the message. Themessagefield and the__str__override are removed;str(ex)is unchanged. -
MissingDependencyErroris aToolingExceptionand anImportError(#591), soexcept ToolingExceptioncatches a missing optional dependency;except ImportErrorstill does. It is deliberately noExceptionBase.ImportError.nameis the missing package.
-
-
pyTooling.Filesystem-
Base.ToTreeis abstract (#552): every concrete element implements it; a derived class withoutToTreeraisesAbstractClassErroron instantiation instead ofNotImplementedErroron the call. -
Element.PathofpyTooling.Filesystemis abstract (#547):Elementand a derived class withoutPathraiseAbstractClassErroron instantiation instead ofNotImplementedErrorwhen read. -
⚠️ The scan and slicing durations are the stopwatch'stimedeltas (#595), which print as h:m:s. -
⚠️ PermissionWarningcarries theDirectorythat couldn't be read (#609) and a message (Directory '<path>' can't be read.), which it didn't have.
-
-
pyTooling.GenericPath -
pyTooling.Graph-
⚠️ The elements take their key-value-pairs frompyTooling.DataModel.KeyValuePairsMixin(#575), withHashablekeys andAnyvalues, instead of each class defining the same dictionary methods. -
⚠️ Iterating aGraphyields its keys (#575), like every element with key-value-pairs. -
⚠️ An edge or link takes its kind as a parameter beforekeyValuePairs(#443):edgeKind/linkKindin the vertex methods,kindin the constructors ofEdgeandLink. -
An edge joining two components of a
Graphmoves the smaller component into the larger one (#502), not always the destination's into the source's (quadratic): a 10000-vertex graph from an edge list takes 55 ms instead of 633 ms, 10000EdgeFromNewVertexcalls 39 ms instead of 1830 ms. Of two equal components, the source's is kept.
-
-
pyTooling.Graph.GraphML-
Attribute values are escaped with
str.translateinstead ofxml.sax.saxutils.escape(#624), whose import loadedurllib.request,ssl,http.client,emailand 80+ more modules:import pyTooling.Graph.GraphMLtakes 28 ms instead of 59 ms (Python 3.15). Output is byte-identical; a short ID takes 141 ns instead of 390 ns. -
GraphML's
Basedeclares its four writing methods abstract (#513):Tag,OpeningTag,ClosingTagandToStringLinesraised a bareNotImplementedError, so an incomplete derived class could be instantiated. An element written in only one way uses@notimplemented. -
GraphMLDocument._XML_NSand_XSI(#613): the namespace constants are package-private and upper case; they werexmlNSandxsi.
-
-
pyTooling.MetaClasses-
ExtendedTypecreates classes faster, and a@readonlyproperty reads faster (issue #531), measured on Python 3.14:- a textual annotation is compiled once per distinct text (#534): 1.0 µs instead of 11.4 µs for a repeated text, 26 to 59 % less time per class with postponed annotations;
- abstract-method implementations are searched only if a base-class has abstract methods (#535): pyVHDLModel's 412 classes 2.2 ms → 0.95 ms;
__new__is recognized as an abstract base-class' replacement without raising and catching anAttributeError(#537): 0.4 µs less per class;- the
__slots__check skips the walk below slottedExtendedTypebase-classes (#538): 10.7 µs → 7.6 µs at hierarchy depth 6; @readonlydeclares its__get__overloads underTYPE_CHECKINGonly (#539): 41 ns instead of 189 ns per read.
-
⚠️ BaseClassIsNotAMixinErroris raised (#505) for a secondary base-class built byExtendedTypewithslots=Truebut withoutmixin=True. It was declared but never raised: with fields, such a class raisedBaseClassWithNonEmptySlotsError, without, it was accepted. -
pyTooling.MetaClassesusesTAttributeFilterofpyTooling.Attributes(#506) instead of its own copy, a union of attribute instances, whileGetMethodsWithAttributestakes attribute classes. -
⚠️ Annotating__weakref__or__dict__raises anExtendedTypeError(#336); both became slots silently, and a__dict__slot gave up what slots save. The exception's note names the keyword to use instead. -
ExtendedTypebinds an annotated class variable before__init_subclass__runs (#373): the hook read the inherited value of a class variable a derived class overrode with an annotation (Sep: ClassVar[str] = "#"inLeaf(Base):":", now"#").SemanticVersion's epoch separator andVersionExpression's per-dialect pattern rely on it.
-
-
pyTooling.Packaging-
DEFAULT_PY_VERSIONSis 3.11 to 3.15 (#491, #557): withoutpythonVersions, a package publishedpython_requires >=3.10, though 3.10 reaches its end of life in October 2026. -
⚠️ A package's license is stated as an SPDX expression only; noLicense ::classifier is added (#338) — closes #219. The classifier made every build warnSetuptoolsDeprecationWarning: License classifiers are deprecated.;licensehas always been filled fromLicense.SPDXIdentifier. A caller's classifier is kept and reported on the console.
-
-
pyTooling.Platform-
FreeBSD is a supported platform (#417):
IsNativeFreeBSDno longer implies x86-64, AArch64 is recognized, andGetMemoryUsagereports the resident size viaresource.getrusage, with0for the virtual size the standard library doesn't expose there. -
⚠️ UnknownPlatformErrortakes one message and carries the unknown value (#594, #668):UnknownValueis the architecture or platform name the system reported; the initializer took*args, unlike every other exception of the package. -
⚠️ UnknownPlatformErroris the base-class of three specific errors (#669), whichPlatformraises instead of it:UnknownOperatingSystemError(os.name),UnknownEnvironmentError(the platform on Windows,sys.platformon POSIX) andUnknownArchitectureError(the processor architecture).except UnknownPlatformErrorstill catches all of them.UnknownOperatingSystemErrorwas aPlatformErrorwithout data.
-
-
pyTooling.ProcessProcessInformation._PROCESS_STATUS_FILEand_PAGE_SIZE(#613) are the class constants' names; they were_processStatusFileand_pageSize.
-
pyTooling.Stopwatch⚠️ Duration,ActivityandInactivityreturn atimedelta(#595); the newDurationInSeconds,ActivityInSecondsandInactivityInSecondsreturn the formerfloat;DurationInNanosecondsandstr()are unchanged. A stopped stopwatch converts its duration once, on first access.
-
pyTooling.TerminalUI-
⚠️ DUNDER_MODULEis the one source of the package's dunder variables (#637):_PrintVersion()and the exception printers read__issue_tracker_url__etc. from it;Noneor a missing variable prints no tracker line. It replaces the class variableISSUE_TRACKER_URLand the module passed to_PrintVersion(module). -
⚠️ TerminalBaseApplication.PrintMissingDependencyError()(aa18469, pushed todevwithout a pull-request) is named after the exception it prints,MissingDependencyError. -
⚠️ HEADLINEandFOREGROUND(#613) are the class constants' names; they wereHeadLineandForeground. -
pyTooling.TerminalUIimports without colorama (#614) and writes uncolored text then: the paletteFOREGROUNDholds empty strings. Without theterminalextra the import raisedMissingDependencyError, so the fallback was never reached._PrintVersiontakes its red from the palette.
-
-
pyTooling.Testing-
⚠️ ApplicationTestcasemerges a test'senvironment(#492) instead of passing it as the whole environment, which lost e.g.SystemRoot(needed by Winsock) on Windows unless the caller wrote{**environ, ...}. Named variables are set,Noneremoves one, the rest is inherited. -
ApplicationTestcasewaits 60 seconds for a program by default (#483), not 10, asRunEntrypointandRunModuletimed out on loaded macOS and PyPy runners. -
ApplicationTestcaseno longer requires_RUNNABLE_MODULE(#426): a program without__main__module is tested through its entry point; onlyRunModuleneeds the module and raises anApplicationTestingErrorwith a note without it. -
⚠️ _CONSOLE_SCRIPTand_RUNNABLE_MODULE(#613) are the class constants' names; they were_consoleScriptand_runnableModule. Test-class constants throughout the testsuite follow the same rule.
-
-
pyTooling.Tracing-
⚠️ Span.Durationreturns atimedelta(#605), asStopwatch.Durationdoes; the newSpan.DurationInSecondsreturns the formerfloat. TheDurationof everypyTooling.CIelement andGanttLayout.Durationaretimedeltas too, each with aDurationInSeconds(new in v10.0.0, so not breaking). -
Span.Format()andTrace.Format()show--for a timespan that never started (#516) instead of raising aTypeErrorthat made the whole trace unprintable.Durationstill raises. -
⚠️ ATrace,SpanorEventtakes its attributes frompyTooling.DataModel.KeyValuePairsMixin(#576), withHashablekeys andAnyvalues; iterating one yields the attribute keys. -
Trace and span identifiers are read from
os.urandom(#632) instead ofsecrets.randbits: 338 instead of 895 ns per identifier, andsecretsloadedrandom,hmac,hashlibandbisect;CategoryStatistics.AverageWaitTime/AverageRunTimeimportstatistics.fmeanwhen called. Import time (Python 3.15):pyTooling.Tracing22.40 → 18.92 ms,pyTooling.Tracing.Render30.19 → 22.49 ms.
-
-
pyTooling.Tree-
⚠️ len()and iteration of aNodeaddress its key-value-pairs (#573), like[],delandin. They addressed the children, whichChildCountandChildrendo now. -
⚠️ Nodetakes its key-value-pairs frompyTooling.DataModel.KeyValuePairsMixin(#574):Node[IDType, ValueType]instead of four type parameters;DictKeyTypeandDictValueTypeare removed.
-
-
pyTooling.URI-
⚠️ The URL module is the packagepyTooling.URI(#662), as it grew beyond a path flavour;pyTooling.GenericPathstays the base of hierarchical paths. -
⚠️ URLderives fromURI(#663):QueryandFragmentcome from the base-class,URL.Schemestays aProtocolsmember,URL.SchemeSpecificPartis//user@host/path; a scheme ofNonewas accepted and raisesValueErrornow.URLhas slots, so an instance has no__dict__. -
⚠️ A repeated query key keeps every value (#660):?tag=a&tag=bkept onlyband was written back as?tag=b; it is aKeySequencePairnow and round-trips.URL(..., query=...)takesQueryParameters and rejects a key given twice. -
⚠️ URL.Parsefollows RFC 3986 (#636): scheme and host required, a path after the host starts with/, forbidden characters and ports above 65535 raiseURLError; user names with.or percent-encoding parse. Every group ofURL_PATTERNwas optional (https://www.müller.de/gave hostwww.m).RESTClientraises its documentedValueErrorfor a non-URL. An empty query (?), empty fragment (#) and a query parameter without=round-trip (#648):?flaghas the valueNone(v9.0.0 raised "not enough values to unpack"). A:after the host without a port, a path in place of the port and a drive letter as host raise their ownURLErrorwith a note giving the probable URL; Git's SCP-likegit@github.com:org/repo.gitgets the note of itsssh://URL (#656).
-
-
pyTooling.Versioning⚠️ A two-part calendar version reads a third numeric part as its build number (#640); a fourth still raises aValueErrorpointing toCalendarVersionandYearMonthDayVersion.
-
General
-
⚠️ Descriptive values every raise passes are required (#634, #668): 51 exceptions and warnings, e.g.ExtraNotFoundError(projectName, version, extra),PackageIndexError(projectName),DuplicateVertexError(vertexID),KeyNotFoundError(key),UnknownPlatformError(unknownValue); a value a raise can't know stays optional (CycleError.Path). -
⚠️ Membership tests are namedContains***(#484):Contains***asks for a key, name or ID,Has***checks a property or feature of an object. -
⚠️ 32 exception classes carry the***Errorsuffix (#320), as PEP 8 asks. -
An annotation admitting
Noneis spelledNullable[...](#549) at the six places that still wroteUnion[..., None]. -
Copies are made with
dict(),list(),set()andtuple()(#540) instead of comprehensions that only copy their input, at 11 sites in 9 modules. -
Two collections are iterated by unpacking instead of
itertools.chain()(#527) inpyTooling.Filesystem,pyTooling.Graph,pyTooling.Graph.GraphMLandpyTooling.MetaClasses;Graph.ContainsVertexByValue()keepschain(), asany()stops at the first match. -
339 f-strings that interpolate nothing lost their
fprefix, across 33 modules (#330). -
An empty line between two consecutive block statements (#479) (
if,for,while,with,try), in 111 places in 38 files.Subgraph.__init__checks itsgraphparameter withif/eliflike every other parameter check.
-
🪳 Bug Fixes
-
pyTooling.AttributesAttribute.GetAttributes(method, includeSubClasses=False)returned the attributes of derived attribute classes too (#489); the parameter was ignored.
-
pyTooling.Attributes.ArgParse-
NamedKeyValuePairsArgument,ShortKeyValueFlagandLongKeyValueFlagparsed no key-value pair (#665), since v9.0.0: they passedFlagArgument's parameterization (action="store_const") toadd_argument, so-Dkey=valueleft-key=valueunprocessed and the handler receivedTrue. The newargparse.ActionKeyValuePairsActioncollects the pairs into a dictionary (-DA=1 -DB->{"A": "1", "B": None}),Noneif the option isn't given; a new parametermetaName(defaultKEY=VALUE) names the value in the help page. -
⚠️ OptionalValuedFlag,ShortOptionalValuedFlagandLongOptionalValuedFlagparsed no value (#673), since v9.0.0: the same boolean-flag parameterization made--colorpassTrueand--color=redan error. Withnargs="?"the handler receives the value (--color=red,--color red), the newdefaultValuefor a bare option, orNonewithout it; a newmetaNamenames the value in the help page.
-
-
pyTooling.CLIAbstraction-
⚠️ A program searched inPATHkept the bare executable name (#495), so a variant built withexecutablePath=program.PathraisedCLIAbstractionError: Program 'git' not found.unless the working directory had a file of that name. -
A program in dry-run mode raised an
AttributeError(#488):Program.__init__andExecutable.StartProcesscalled a nonexistentLogDryRun.Program.LogDryRunrecords the skipped action inProgram.DryRunMessages; a derived class may override it. -
Four rows of the CLI argument prefix table were wrong (#352):
ShortBooleanFlagwrites-with-flag/-without-flag, not-flag/-no-flag, and the three Windows valued-flag classes write/flag:value, not/flag=value. Three:class:references pointed at the wrong module and rendered as plain text.
-
-
pyTooling.Cartesian2D-
LineSegment2D.Lengthsquared the x-difference twice and ignored the y-difference (#631), wrong whenever both differ in magnitude: a vertical segment had length0.0,(0, 0) → (3, 4)4.24instead of5.0. SoLineSegment2D.AngleToraisedZeroDivisionErrorfor a vertical segment and returned wrong angles otherwise;RectangleandSquare(pyTooling.Cartesian2D.Shapes) raisedZeroDivisionErrorfor every shape with an edge parallel to the y-axis, andSquarerejected rotated squares.LineSegment3D.Lengthwas correct. -
LineSegment2D.AngleToandLineSegment3D.AngleTodivided by both lengths and calledmath.acos(#641): a zero-length segment raisedZeroDivisionError, and parallel segments raisedValueError: math domain errorwhen the normalized scalar product rounded outside -1..1 (22678 of 100000 random parallel 2D float pairs); elsewhere up to2.1e-08instead of0.0. The angle isatan2(|cross product|, scalar product)now - exactly0.0,pi / 2andpifor integer coordinates - and a zero-length segment raisesValueErrorwith a note. Both checkother(#652):NoneraisesValueError, a non-segmentTypeError, instead of anAttributeError.
-
-
pyTooling.Cartesian2D.ShapesRectangleaccepted a parallelogram (#633), raising only if all four angles were0.0, andSquarea rhombus. Two adjacent inner angles must be 90° (math.isclose), which also rejects crossed quadrilaterals; edge lengths compare withisclose, so a rotated rectangle with float corners passes; coincident corners raiseValueErrorinstead ofZeroDivisionError. Found by the testcases of #629.
-
pyTooling.Configuration-
A key stated without a value raised an
UnsupportedValueTypeError(#490) -key:in YAML,nullin JSON. It reads asNone;get()returns its default only for an absent key; a variable referencing such a key raises aPathExpressionErrorinstead of becoming"None". -
An empty YAML file crashed instead of reading as an empty configuration (#376), with
AttributeError: 'NoneType' object has no attribute 'keys'. -
..above a configuration's root crashed with anAttributeError(#368), inQueryPath("..:key")and in a top-level${..:key}reference; it raises aPathExpressionErrornow. Also,Configuration.__init__overwrote the root node's_rootwithNone, and nested nodes referred to their parent as root; every node refers to the configuration now.
-
-
pyTooling.Decorators- A type checker rejected every function decorated with
@export(#550): its type variable was bound totypeorFunctionType, which doesn't match adef; bound totypeorCallablenow, the decorated object keeps its signature.
- A type checker rejected every function decorated with
-
pyTooling.Dependency-
Project.DownloadDetails()andRelease.DownloadDetails()parsed an error page as JSON for an HTTP error status other than 404 (#583), turning a server error into a JSON decode error or a project or release filled from an error document. Both raise aPackageIndexErrornaming the status and the requested project or release. -
Release.__init__couldn't copy itsrequirements(#540): it unpacked each key of the mapping into two, so{"test": [...]}raised aValueError. No caller in pyTooling passes it yet. -
Every release of a project was described by the latest release's metadata (#362):
Project.DownloadReleaseDetails()fetched the project endpoint per release (N identical requests);requests2.20.0 reported 2.32.0's dependencies (charset-normalizerinstead ofchardet).Release.DownloadDetails()was always right. -
An environment marker was read as a broken extra (#362), e.g.
importlib-resources ~= 3.0; python_version < "3.7". Releases before core-metadata 2.1 have noprovides_extra, so everyextra == "…"requirement fell through (sphinx_design0.0.1 kept 1 of 11). Extras are recovered from the markers naming them; a declaredprovides_extrastays authoritative. -
The package index downloaded and replaced projects it had loaded already (#597):
Release.DownloadDetailspassedTrueasLazyLoaderStateand registered a new, unloaded project per requirement; a project failing with404 Not Foundstayed registered;Project/Releasere-downloaded their details for a higher load state.
-
-
pyTooling.Filesystem-
Directory.ToTree()marked a directory's node as a file and dropped its files and symbolic links (#554): the node is aNodeKind.Directorywith subdirectories, files and symbolic links as children.Filename.ToTree()andSymbolicLink.ToTree()take the optionalformattoo, so a user-defined format renders the whole tree. -
Directory.ResolveSymbolicLinks()raisedAttributeErrorfor a relative link target through another symbolic link or a file (#647), e.g.dirlink/f.txtwithdirlink -> dir, failing a scan of/usr/share. A link inside a target's path is replaced by its target, as the OS does; a file inside it and a link cycle register the link as broken, a link through one with an absolute target as unconnected.
-
-
pyTooling.Graph-
Graph field annotations resolved to a property, not to a class (#512):
GraphandComponentare also property names in five classes, sotyping.get_type_hints(cls, vars(pyTooling.Graph))raisedTypeErroror silently resolved to the property; the fields use the aliases_Graphand_Component.LinkedList.NodeannotatedLinkedListwith one of its two type parameters (TypeError: Too few arguments). -
pyTooling.Graph's field annotations didn't match the classes' type parameters (#445): value and weight were swapped in theEdge[...]/Link[...]subscripts, andtyping.get_type_hints(Vertex)raisedTypeError: Too few arguments for BaseGraph.
-
-
pyTooling.Graph.GraphML-
GraphML: a link between two subgraphs was lost (#511): it is stored in the two subgraphs, but only the graph's links were read. The subgraph translation, a copy of the graph's, also wrote
Nonevalues and declared a key per vertex and edge; both share one translation now. -
GraphML: a vertex or tree node without an ID was written as
<node id="None">(#510), all with the same ID, connecting edges by it. Such a node gets a stable generated IDvertex<number>, as in the DOT export. -
Of several GraphML edges without an ID, only the last added was written (#509), e.g. with
EdgeToVertexwithoutedgeID. They are kept inBaseGraph.EdgesWithoutIDand written before the edges with an ID. An element's ID is annotatedNullable[str]. -
The GraphML writer wrote
Noneand invalid XML (#500):<data key="nodeValue">None</data>for an element without a value,id="None"for an edge without an ID, and unescaped IDs and key names, so&,<or"invalidated the document.
-
-
pyTooling.LinkedList-
A linked-list node ignored its key (#470):
Node.__init__stored the value asKey, so a key given to a node was lost. -
LinkedList.Clear()left its nodes linked (#619) to the list and their neighbours, so re-inserting one raisedNotInSameListError, and the nodes formed a cycle only the garbage collector freed.Clear()unlinks every node now. -
LinkedList.GetNodeByIndex()documented an exception it never raises, and two methods raised exceptions no:raises:field mentioned (#322).
-
-
pyTooling.MetaClasses-
sphinx_autodoc_typehints warned twice per slotted class (#658):
ExtendedType's generated__getstate__and__setstate__were namedExtendedType.__new__.<locals>.__getstate__inpyTooling.MetaClasses, skipped with "Cannot handle as a local function". They carry the class' module and qualified name (Node.__getstate__) now; creating a slotted class takes 0.3 to 0.5 µs longer. -
⚠️ ExtendedTypeignoredslots=Truesilently when the primary base-class had no slots (#638): no__slots__, instances got a__dict__, despite the documented error. It raisesBaseClassWithoutSlotsErrornow; class creation time is unchanged. -
singleton=Truewas ignored on a class derived from a non-singletonExtendedTypeclass (#646): the inherited__isSingleton__(False) won, soclass Final(Base, singleton=True)gaveFinal() is Final()→False. A class derived from a singleton stays one, also withsingleton=False(documented on the MetaClasses page). -
@singleton,@mixinand@slotteddropped a class' own options (#646): recreating anExtendedTypeclass with only their own option,@singletonon aslots=Trueclass returned one neither slotted nor singleton, on a mixin-class a non-mixin. The class keepsslots,mixin,weakrefandsingleton; the decorator adds its option. -
On Python 3.14+,
ExtendedTyperaisedNotImplementedErrorfor a field annotation naming something undefined (#623) withoutfrom __future__ import annotations, also for a self-reference like_parent: Node: it called__annotate__withFormat.STRING;annotationlib.call_annotate_function()builds the strings now. Python 3.11-3.13 are unaffected. -
A slotted class naming
objectas its base-class raised anIndexError(#546):class C(object, metaclass=ExtendedType, slots=True). An explicitobjectbase-class behaves like none. -
⚠️ AClassVarnaming a mixin-class' slot produced a broken class (#373): the name was in__slots__and a class attribute, so the field was read-only (AttributeError: … attribute '_shared' is read-only). It raisesDuplicateFieldInSlotsErrornow. -
⚠️ A mixin-class' field was added to the slots even if that name was a slot already (#504): two slots of one name, or for__weakref__Python'sTypeError: __weakref__ slot disallowed. It raises aDuplicateFieldInSlotsErrornow. A field two mixin-classes inherit from one mixin-class is one slot now, not two. -
The initial value of a slotted field was dropped (#524):
_name: str = "root"withslots=Trueormixin=Trueraised anAttributeErrorwhen read before__init__assigned it. An intermediate__init__assigns the hierarchy's values before any__init__runs, also if a derived__init__skipssuper().__init__()(#544: 916 → 438 ns per instance with one initial value, 3.14).__allSlotDefaults__keeps the hierarchy's initial values; a hierarchy without any keeps its own__init__(#553). -
An abstract property didn't make its class abstract (#508):
ExtendedTypelooked for@abstractmethod/@mustoverrideon the property, not its getter, and a property implementing an abstract method didn't count.pyTooling.Licensing.BaseLicenseis abstract now, as its abstract properties say. -
A class or static method marked abstract didn't make its class abstract (#503):
ExtendedTypelooked for the marker on theclassmethod/staticmethodobject, not the wrapped function.Versionis abstract now, as its abstractParsesays.
-
-
pyTooling.PackagingloadRequirementsFilekept a comment behind a requirement (#491), so an extra's metadata - pyTooling's owntestextra among them - carried e.g.colorama ~= 0.4.6 # 'terminal'.
-
pyTooling.Stopwatch-
Stopwatch.ExcluderaisedUnboundLocalErroron its second use (#365): only the uncached path bound the returned context manager, so a loop died on iteration two. -
Stopwatch.Start()on a stopped stopwatch raised "Stopwatch was already started." (#655), thoughIsStartedisFalseafterStop(); it raises "Stopwatch was already used (started and stopped)." with a note to create a new one. -
A failing
Stopwatch.Stop()changed the stopwatch (#655): a secondStop()replaced the first one'sStopTime, and aStop()beforeStart()left a stop time behind, so the followingStart()raised.
-
-
pyTooling.TerminalUI- A message's indentation was recorded but never printed (#325):
Line._indent— theindentparameter of everyWrite*method plusBaseIndent— never reached the output.
- A message's indentation was recorded but never printed (#325):
-
pyTooling.Testing- pytest tried to collect
TestingError(#483), as its name starts withTest.__test__ = Falsekeeps it out.pyproject.tomldrops the optionjunit_xml, which pytest doesn't know.
- pytest tried to collect
-
pyTooling.Tree- Splitting a tree by
node.Parent = Noneleft the split-off IDs registered in the old tree (#580), whereGetNodeByIDandSizestill found them, the new tree's registry empty. They move to the new tree now.
- Splitting a tree by
-
pyTooling.URI-
⚠️ Hostaccepted an empty host name, andNoneraised aTypeError(#485), though its doc-string promised aValueErrorfor both. Parsing is unchanged. -
pyTooling.URIraised a bareToolingException(#409); it raisesURLErrornow, naming the first character RFC 3986 forbids with its percent-encoding, and the known schemes.
-
-
pyTooling.Versioning-
⚠️ A calendar version ignored everything but its numbers (#640):YearMonthVersion(2025, 6, postfix="a")equalledYearMonthVersion(2025, 6), hashed alike and was written2025.6; a build number didn't count either.CalendarVersion.Parserejected every release level, post-release, development release and postfix (2024.01a,2024.01-dev,2024.10.15+deadbeef, OSVVM's2025.06a). -
⚠️ A postfix didn't order a version (#640):1.2.3and1.2.3+build.5were unequal, yet neither was less and both<=and>=were true, so sorting depended on input order. No postfix is less than any now; postfixes compare as strings (2025.06 < 2025.06a < 2025.06b). -
⚠️ SemanticVersion.Parseread SemVer'sv2.0.0-rc.1as release candidate 0 with the postfix1(#640), and rejected a postfix with a.or-(v1.2.3+build.5), which SemVer's build metadata allows. -
Version.__hash__hashed the prefix and the flags, which==doesn't compare (#640):SemanticVersion(1, 2, 3) == SemanticVersion.Parse("v1.2.3"), but asetholding one didn't find the other. -
str(ReleaseLevel.Gamma)raised aToolingException(#640), soformat(version, "%R")failed for a gamma release. -
Comparing a
YearMonthVersion,YearWeekVersion,YearReleaseVersionorYearMonthDayVersionwith anintraisedTypeError(#666): the operand was built ascls(major=other), and these classes takeyear.YearMonthVersion(2024, 1) > 2024works now. -
%%was no literal percent sign in a format specification, and onlySemanticVersionchecked the specifiers (#666, #671):f"{version:%%M}"raisedUnknown format specifier '%1', a trailing%raisedIndexError, a calendar version passed an unknown specifier through (%M.%Xgave2024.%X), and a%in a value (postfixa%b) was taken for an unknown specifier.%%M %M.%mgives%M 1.2; every version class raisesValueErrorfor an unknown specifier or a trailing%. -
SemanticVersion.Parse("1.2.3.4-rc1")read the release level as a postfix (#666): build 4 and release candidate 1 now. ADevelopmentrelease level with anumberdropped the number (1.2.3-dev); it raisesValueErrornow. -
⚠️ SemanticVersion.Parselost a number followed by letters (#672):1.2.3foowas1.2with the postfix3foo- the pattern backtracked and took.3fooas a postfix - and1.2.3.4.5was build 4 with the postfix5. A postfix after a.doesn't start with a digit now, so these are syntax errors. -
repr()of aSemanticVersionended after the micro number (#666):1.2.3-rc1.post2.dev3+abcwas1.2.3; it writes every part.repr()of aPythonVersionis its PEP 440 normalized form. -
VersionSetgiven an iterator was empty (#666): the type check consumed it. -
⚠️ A release level without a number wasn't one (#494):PythonVersion.Parse("1.0a")didn't parse, andSemanticVersionread-alphaas a postfix; PEP 440 normalizes1.0ato1.0a0, SemVer reads-alphaas a pre-release.PythonVersionalso wrote a fourth numeric component (1.2.3.4) as1.2.3, though it kept and compared it. -
⚠️ PythonVersion.Parsemisread most spellings PEP 440 normalizes (#517): throughSemanticVersion's pattern,1.0rev1became1+0rev1,1.0-11.0+1,1.0.0-rc.11.0.0rc0+1and more, while1.0.post,1.0.0-rc.1.dev2and1.0+Ubuntu-1raisedValueError. Its own pattern now reads.,-or_around a release level, post- and development release,rev/rforpost, the implicit post-release1.0-1and implicit numbers, case-insensitive; a local version compares normalized.⚠️ A local version after.and the levelsfinal/plare rejected, as PEP 440 has neither. -
⚠️ Versioncompared a dev or post part's number, but not whether it was present (#436), so1.0.dev0 == 1.0. PEP 440 orders1.0.dev0 < 1.0 < 1.0.post0. -
⚠️ VersionRange.__and__discarded both operands' bound handling (#372): the result defaulted toBothBoundsInclusive, so intersecting two ranges that exclude their bounds included them, and a version outside both operands tested inside. Each bound keeps its range's handling; for equal values the stricter one wins.
-
-
pyTooling.Warning-
A supervised warning collector raised a
TypeError(#469) when left after collecting a warning: it calledThreadSupervisor.AddWarnings()without the thread's name, and with an exception passed an empty name, soReRaise()named no thread. Both passcurrent_thread().namenow. -
WarningCollector.Raisetook anAttributeErrorof a collector's handler for a missing collector (#562), swallowing it for a warning or turning it intoUnhandledCriticalWarningError/UnhandledExceptionErrorfor a critical warning or an exception. The collector is looked up withgetattr(..., None); the handler's exception propagates.
-
-
General
-
Building the documentation on Windows ended in a
UnicodeEncodeError(#385) rather than in a build;run.ps1builds in UTF-8 mode. -
22 parameters were annotated non-optional but defaulted to
None(#354) -Packaging(14),Configuration(4),Graph(2),StopwatchandTree- an implicitOptionalPEP 484 prohibits. All areNullable[...]now;mypy --strictgoes 820 → 797, no implicit-Optionalnotes left. -
doc/conf.pyimportedpyTooling.Packagingbefore inserting the repository intosys.path(#328), so nine modules were documented from the installed package and the rest from the checkout. -
39 field annotations in 27 classes resolved to a property, not to a class (#667), as #512 fixed for
pyTooling.Graph: a field annotated with a class whose name is also a property of its class -RequirementsFile._path: Pathnext toRequirementsFile.Path- evaluated to the property ininspect.get_annotations(cls, eval_str=True)andtyping.get_type_hints(cls, vars(module)); inpyTooling.Platform, without postponed annotations, Python 3.14+ returned the properties even fromget_type_hints(Platform). The fields use module-level aliases (_Path = Path) inCI,CLIAbstraction,Dependency,Diagram.Gantt,Filesystem,Graph,Platform,TerminalUI,TracingandVersioning.
-
📖 Documentation
-
pyTooling.Attributes- The Attributes and Configuration overviews and the ArgParse page are written (#393, #394, #395); Filtering Attributes documented a
predicateparameter onlyGetMethodsWithAttributeshas. #351 adds the ArgParse page's example programs (attributes and plainargparse, side by side) and completes the Attributes documentation.
- The Attributes and Configuration overviews and the ArgParse page are written (#393, #394, #395); Filtering Attributes documented a
-
pyTooling.Attributes.ArgParse- The eighteen ArgParse constructors document their parameters (#323); each carried the same sentence describing
*args/**kwargs, which none of them takes.
- The eighteen ArgParse constructors document their parameters (#323); each carried the same sentence describing
-
pyTooling.Cartesian- The Cartesian pages are written (#396).
-
pyTooling.CI- The Pipeline page has a section Rules (#570): evaluation order, what a
RuleOutcomedecides, an example; the GitHub and GitLab mappings name whereif:,rules:,rules:ifandrules:whengo.
- The Pipeline page has a section Rules (#570): evaluation order, what a
-
pyTooling.CLIAbstraction-
The Program page and the version-constraint reference are written (#397, #398): five open sections on
pyTooling.CLIAbstraction.Program, and what a version constraint, an epoch and a validator are inpyTooling.Versioning. -
The CLI argument reference is written (#352): six sections state what to assign, what comes out, and what decides between a class and its neighbour (e.g. single value vs. tuple).
-
-
pyTooling.Dependency-
The Exceptions and Warnings section lists
UnsupportedVersionWarningas a critical warning (#581), andPackageIndexError(#583). The intersphinx mapping includesrequests, soHTTPErrorlinks to its documentation. -
An info box explains why the dependency warnings are critical (#564): data must never disappear without a report. The example collecting the warnings imports
Path. -
The package-override file states the version range a license was checked for (#486):
<package> >=<checked>, <<next major>, so an unchecked release warns again. docutils isBSD-2-Clause AND BSD-3-Clause; cycler, kiwisolver and sphinxcontrib-jquery have overrides; only pillow still needs one, whichMIT-CMUresolves.
-
-
pyTooling.DataModel -
pyTooling.Exceptions -
pyTooling.Filesystem -
pyTooling.Graph -
pyTooling.MetaClasses-
The Meta-Classes background page is written (#358): what a
classstatement calls, the five steps of class creation, lighter tools to try first (class decorator,__init_subclass__,__set_name__,dataclass_transform), whatExtendedTypeadds, why slots need a meta-class. -
ExtendedType's reference (#432): the Slotted and Mixin sections have an example and their rules, each with its exception; a table lists the class keyword arguments; a duplicated slotted section is merged, and the mixin tab ofdoc/index.rstis written. -
Weak References (#336):
doc/MetaClasses.rstexplains why a slotted class can't be referenced weakly,weakref=True, the once-per-hierarchy rule and mixin-classes. -
Naming the expected members with a protocol (#611): a new subsection of Expected Members; the keyword table names both forms.
-
-
pyTooling.Stopwatch-
The Stopwatch page shows how to measure again (#655): a stopped stopwatch can't be restarted or reset; Measuring Again creates one per measurement, or adds regions up by pause/resume or
preferPause=True. The class doc-string says so; entering a stopped stopwatch raises with that note. -
The Stopwatch page explains three durations (#595): The Three Durations (was The Two Durations) shows
Durationas atimedeltaand the...InSecondsfloats, and the split-time example uses them. -
Stopwatch.__enter__and__exit__document each condition they raise for (#520);__getitem__documented aKeyError, but raises anIndexError. -
The Stopwatch page is written (#365): ten subsections instead of four
tbdentries, including the split-time model - every operation ending a span records a split time, tagged activity or inactivity.
-
-
pyTooling.TerminalUI-
Command handlers in mixin-classes (#611): the Terminal page's section A Complete Application shows a handler mixin-class expecting
TerminalApplicationProtocol. -
colorama is optional (#614): the module doc-string,
MISSING_DEPENDENCY_EXIT_CODE,PrintMissingDependencyErrorand the Terminal page say the module imports without it; the exit-code block is aligned.
-
-
pyTooling.Tree- New section Deleting a Tree (#620) on
Node.Delete().
- New section Deleting a Tree (#620) on
-
pyTooling.Warning-
Designing Warning Classes has a hint box Warning or critical warning? (#564): decide by what a caller who never sees the warning gets - a complete result (
Warning) or one silently lacking something it relies on (CriticalWarning). -
The warnings page explains collecting, escalating and documenting warnings (#563): warning kinds, raising, collecting, escalation, applications (
setup.py, terminal applications), threads, designing warning classes; its example raised Python's built-inWarningand didn't run.pyTooling.Warning's doc-strings are corrected (e.g.CriticalWarning's inverted tip). Functions inFilesystem,DependencyandMetaClassesname the warnings they report; each warning class states whether it's critical (.. important::) or not (.. attention::);:raises:listsUnhandledCriticalWarningError/UnhandledExceptionErrorwhere they can result.
-
-
General
-
The documentation coverage page names its tool (#625): pyTooling.Sphinx'
report:doc-coverage, reading the source withpyEDAA.Reports.DocumentationCoverage.Python.Analyzer, not docstr-coverage. -
News lists the final outcome since v9.0.0, as these notes do (#586): every
⚠️ item is in Breaking Changes and in its own section. -
A parameter's description opens with Optional, exactly when it has a default (#545): 25 parameters gained it, 56 without a default lost it.
-
Hand-drawn hierarchies are drawn by pyTooling.Sphinx'
treedirective (#541): the CI pipeline, the tracing data model and the tree example; their classes link to the API reference. -
A doc-string's summary names no example (#542): in 8 doc-strings, the example moved into the description.
-
The Dependency page renders every extra's table from the package index (#569, #528) with pyTooling.Sphinx'
dependency-tabledirective, expanded without a depth limit. -
The report pages are rendered by pyTooling.Sphinx' domain
report(#526) instead of the extensionsphinx_reports;doc/requirements.txttakespyTooling.Sphinx[reports]. -
Two doc-strings didn't parse as ReST (#521):
lazy's usage list was folded into its summary, and the WindowsGetMemoryUsagehad an unexpectedly indented line. -
The README - the PyPI page - no longer describes what the package doesn't have (#501): exceptions carry their
***Errornames, the Terminal example is the landing page's (CheckPythonVersionwent in v9.0.0),ObjectWithSlotsisSlottedObject, the CLI abstraction claims no escaping. Seven dead anchors and eight link labels without target are repaired; the landing page's Terminal example importsNoReturn. -
Wrong statements in the documentation are fixed, and the tracing page has a data model (#487), e.g. an uncollected warning is dropped; both configuration backends interpolate and return every scalar as
str;pyTooling.Dependencydoesn't build onpyTooling.Graph; the CLIAbstraction example uses only existing methods. Every__enter__/__exit__doc-string opens withEnter/Exit the context and ...; GitHub links use theghrole. -
Doc-strings state what the code does (#485):
Attribute.GetClasses'subclassOffilters by the annotated classes' base class;PrintMissingDependencyError's code example broke into lines inside its code block; a continuation line ofGraphML.Graph.OpeningTagat column 0 indented the whole doc-string. -
The published documentation counts its page views with GoatCounter (#481):
doc/conf.pyadds GoatCounter's script (sitepytooling) only to builds on GitHub Actions. -
Pages compare pyTooling with competing solutions: the pipeline model (#453), Tracing (#454), Warning (#455), TerminalUI (#456),
ExtendedType(#457), Attributes (#458), REST (#459), Licensing (#460), Configuration (#461), Dependency (#462), Versioning (#464), the test annotations (#465), the stopwatch (#466), LinkedList (#467) and CLIAbstraction (#468): per competitor its disadvantages, the overlap and where it is ahead. Tools not usable from Python are named in each section's introduction (#472). -
Two tutorials are written — Attributes (#355) and CLI Abstraction (#356) — and the tutorials get an overview page (#359).
-
The glossary is written (#399, #415): 47 → 122 terms (102 entries, some with an alternative name), all written and cross-linked - 17 had only undocumented as body. WSL is the Windows Subsystem for Linux, SPDX the Software Package Data Exchange.
-
Three modules say in their doc-string that importing them needs an extra (#380).
pyTooling.Dependency.Python,pyTooling.Configuration.YAMLandpyTooling.TerminalUIguard a module-level import and raiseMissingDependencyError. -
The Exception Hierarchies tutorial is written (#357): six steps, from one base exception per package to documenting every exception that escapes.
-
The release history is written (#342): 50 entries back to v0.5.0, structured like these notes; this release's entry is filled in.
-
Schemas are published for a human to read (#343): a page per schema with its full source, a copy button, a download and an
xmlschema-graphdrawing by pyTooling.Sphinx; the package-override JSON schema too (#389). Both are served at their$idURL underhttps://pyTooling.GitHub.io/pyTooling/Schemas/. -
The documentation-coverage backlog is at four real problems (#324), all fixed; 12 of the 18 remaining reports are tool artifacts.
-
🧪 Unit Tests
-
3461 passed, 30 skipped, 3 xfailed, 1317 subtests — up from 1326 and 93 subtests at v9.0.0.
-
pyTooling.Cartesian2D,pyTooling.Cartesian3DRectangle,Square,Volume,Cuboid,TrapeziumandCubeare tested (#629), so areLineSegment2D.Length/AngleTo,LineSegment3D.Lengthand axis-aligned and rotated rectangles and squares (#631), andAngleToof parallel, opposite and zero-length segments in 2D and 3D (#641). Type checks of points, offsets and sizes assert their messages and notes (#657).
-
pyTooling.MetaClasses-
Deferred annotations on Python 3.14+ (#623):
DeferredAnnotationscompiles class bodies without the future import - a self-reference, an undefined name and aClassVarof an undefined name. -
The generated
__getstate__/__setstate__are named like methods of their class (#658), also for a derived and a nested class. -
A pickled graph is checked after unpickling (#629):
PickledGraph.test_SimpleGraphasserted on the original graph; new testcases compare every vertex and edge.
-
-
pyTooling.Stopwatch- The
Stopwatchtests allow a 4 times longer sleep on GitHub Actions (#525), where shared runners overshot a 0.5 s sleep by up to 3x; local runs keep the narrow limit.
- The
-
pyTooling.Tracing-
pyTooling.Tracing.CIhas its own test module (#657): the attribute keys,SpanKindandResult, and every timespan - kind, parent, the attributes its parameters set, skipped unknown values. -
Reading OTLP/JSON is tested (#348):
tests/unit/Tracing/OpenTelemetry.pygrows from 50 to 191 testcases - byte-identical round-trip, 2000 nested spans without recursion, schema violations as notes, fragments and collections merged in any order, the errors and their generic bases. They are skipped wherejsonschemacan't be installed: PyPy on Windows.
-
-
General
-
The last 14 f-strings in the tests that interpolate nothing lost their
fprefix (#518). -
URL.Host's parameter checks are tested, and the classifier test skips withouttrove-classifiers(#485) instead of failing. -
The suite runs in parallel with pytest-xdist (#433): 17 s instead of 31 s; two terminal tests accept a worker's 80×24 characters. CI too since #434 (
-n auto); Coverage.py follows the workers (patch = ["subprocess"]), 83 % either way; #435 limits the measurement to the package (include = ["pyTooling/*"]). -
Five FigLet banners named a module the file has nothing to do with (#347), copied along with the file they were copied from.
-
The matplotlib testcases are skipped on PyPy 8.0 on Linux (#474) (no Pillow wheel, building one fails), and since #558 on macOS (no numpy
pp80-darwinwheel), untilpp80wheels ship.
-
-
pyTooling.TerminalUI-
TerminalApplicationProtocolstays in step withTerminalApplication(#611): every member exists with the same signature (methods) or return type (properties) and carries a doc-string;expectswith a protocol, a derived protocol, a plain class, a protocol's implementation andtyping.Protocolitself. -
The palette with and without colorama (#614): the module loaded with colorama blocked has empty palette entries.
-
⚙️ GitHub Pipeline
-
Only the wheel is published on PyPI (#582):
publish_sdist: 'false', an input of pyTooling/Actions v9.3.0. The sdist can't build a wheel (setup.pyreadsrequirements.txt, which it lacks); the pure-Python wheel installs everywhere. -
Artifacts are cleaned up by
CleanupArtifacts.yml(28c5b7e, pushed todevwithout a pull-request):IntermediateCleanUpandArtifactCleanUpno longer call the deprecatedIntermediateCleanUp.ymlandArtifactCleanUp.ymland name the artifacts by the keys ofParameters.yml'sartifact_names, likeCompletePipeline.yml.IntermediateCleanUpdeletes the platform tests' artifacts, keeping the package artifact on tag runs forPublishOnPyPI. Both run unless the pipeline is cancelled. -
The single jobs and mypy use Python 3.15 (3ae2c76, pushed to
devwithout a pull-request): documentation, checks and packaging usepyTooling/Actions@r9's default; mypy checks withpython_version = "3.15". -
The pipelines use
pyTooling/Actions@r9(#390, #543) instead of@r7, inPipeline.yml,Benchmark.ymlandPerformance.yml: MSYS2 runs on UCRT64 and Clang64 only, as Actions v9.0.0 removedmingw32andmingw64. -
Python 3.15 and PyPy 3.12 are tested (#557, #558): CPython 3.11 to 3.15, PyPy 3.11 and 3.12. PyPy isn't run on Windows on ARM.
-
A package PyPI would reject is neither tagged nor uploaded (#560):
Packagereportspypi_compliant(pyTooling/Actions v9.1.0);TriggerTaggedReleasefails before tagging,PublishOnPyPIchecks again; before, a release PyPI refused was tagged and its release page published anyway. -
A release version that doesn't match the code, or that PyPI already has, is neither tagged nor published (#588): new job
VersionCheck(CheckReleaseVersion.yml@r9, as inCompletePipeline.yml) compares it with__version__inpyTooling/Common/__init__.pyand, on the release commit, asks PyPI;TriggerTaggedRelease,ReleasePageandPublishOnPyPIrequire its success. -
A pre-release's release page is published as a pre-release (#437).
ReleasePagepassesprereleaseandlatestfromPrepare's new outputis_prerelease(pyTooling/Actions v8.5.0), so a tag likev10.0.0-rc1doesn't replace the latest release.
Others
-
pyTooling.MetaClassesExtendedType's meta-class parameter is namedmetaclseverywhere (#507);__new__and three classmethods called itself.
-
General
-
f-strings without placeholder are plain strings (#590):
f"…"became"…"in 4 files (10 strings); every string keeps its value. -
One
from <module> import ...statement per module and scope (#630), continued by\and aligned to the first imported name: 624 → 585 import statements in the package, 705 → 642 in the tests; needlessly function-local imports moved to the module's import block. Names, order and namespaces are unchanged.pyTooling.Packagingseparates its stdlib and pyTooling import groups by a blank line and importsSizedfromtyping(#654). -
Unused imports are removed (#628): one name each in
CallGraph,ControlFlow,Dependency.Python,Diagram.GanttandTracing.Render.Matplotlib, two inFilesystem, and 12 in seven test modules; no other module imports them from there. -
Version
10.0.0(0f68e7b), pushed todevdirectly; earlier version bumps went through #555 and #556. -
doc/make.batruns Sphinx with Python 3.15 (4ed65b8, pushed todevwithout a pull-request):py -3.15. -
A Prospector profile (#522),
.prospector.yml, for Codacy: tabs for indentation, D213 instead of the contradicting D212, 120 columns.
-
Related Issues and Pull-Requests
Merged into this release and not named in a section above, newest first:
- #670 -
Platform.StaticLibraryExtension's doc-string namesafor macOS, as the code returns. - #651 - the Tree Rendering example shows the current markers, and a table explains
Render()'s parameters. - #650 -
Rule.Whenis annotated-> RuleOutcome; it named a nonexistentWhen. - #596 - two
#:field comments ofpyTooling.MetaClassesare aligned with their block. - #645 - one
:raises:field per parameter: 12 fields named several parameters as alternatives. - #643 - every module variable is documented by a doc-string below it: 42 variables in 14 modules carried a
#:comment line above or a trailing#:comment. - #642 - the package-override schema tests are skipped where
jsonschemacan't be installed: PyPy on Windows, whererpds-pyhas no wheel. - #589 -
str()of aKeyNotFoundErroris its message, without the quotes #584 had added. - #567 - name no enumeration members in the doc-strings of
Span.StateandciSpanFilter, both new in this release. - #551 - name the override's value and file in the warning for a license override
LicenseExpressioncan't parse; license overrides are new in this release (#363). - #493 - align the guarded imports with the module's import column.
- #482 - link pyTooling.GitHub's trace page by its new label.
- #452 -
pyTooling.CI'sBasechecks only a parent's type, as the root takes no parent. - #447 - fix a malformed table and a label without a blank line in the documentation.
- #387 - state the licenses the package index can't answer for in the package-override file.
- #364 - generate the dependency tables instead of maintaining them by hand. Partly moved to pyTooling.Sphinx.
- #363 - resolve a release's license, and say so when it can't be resolved.
- #326 - closed, integrated into #329.
- #428 - closed by GitHub during a re-base, replaced by #429.
Moved to pyTooling.Sphinx, newest first:
- #463 - compare the Sphinx extension with competing solutions.
- #471 -
DependencyTable's configuration values as a class member of the directive. - #431 - document the roles and directives of the Sphinx extension.
- #392 - render the badges with a directive, its coordinates stated as options.
- #391 - the
xsd-graphdirective moves intopyTooling.Documentation.Sphinx. - #381 - say why a package needs a license override (
UnresolvedLicenses). - #379 -
:version-format:and:dependency-format:. - #378 - rename the configuration, take iterables, stop capping the depth.
- #360 - one Sphinx extension for the whole family.
- #349 -
.. condensed-class::renders an interface from source. - #344 -
.. xsd-graph::draws an XML schema.
Moved to pyTooling.GitHub, newest first:
- #473 - compare the
ghadomain's warnings without the temporary path on macOS and Windows too. - #442 - summarize a workflow with
gha:parameter-table,gha:interface,gha:dependencies,gha:yamlandgha:autoinputs. - #441 - draw a workflow's pipeline with
gha:pipeline-graph. - #440 - a Sphinx domain
ghafor GitHub Actions workflows. - #451 - build a pipeline from a workflow file, and give a run the dependencies of its file.
- #450 - model a job's steps and matrix, and read the reusable workflows a workflow calls.
- #449 - model the jobs of a workflow file, and the reusable workflows they call.
- #448 - model the parameters of a reusable workflow.
- #439 - read a GitHub Actions workflow file: its triggers and permissions.
- #425 - draw the pipeline:
--gantton thepipelinecommand. - #423 - pyTooling's program: the
pyToolingcommand line interface. - #411 -
Workflowdeclares its own parent type. - #407 - a data model of a GitHub Actions workflow run.
Issues addressed in this release:
- #15 - implement more SPDX identifiers and licenses.
- #49 -
__weakref__in types with slots, implemented in #336. - #219 - the deprecated license classifiers.
- #246 - JSON output for OpenTelemetry and Jaeger.
- #346 - SPDX license expressions, implemented in #367.
- #384 - enforce an attribute's scope, implemented in #489.
Still open (a closes keyword fires only on a merge into the default branch); they close with this release:
- #18 - extract the description from the first paragraph of a package's doc-string.
- #383 - an attribute's scope as a class keyword argument, implemented in #523.
- #612 -
expectsaccepts atyping.Protocol, implemented in #611.
Published from Pipeline workflow triggered by github-actions[bot] on 2026-10-26 18:41:19 UTC.
This automatic release was created by pyTooling/Actions::PublishReleaseNotes.yml