5afbafb6dde0b144cca0fe3f377a5b284e3561d3 max Mon Sep 14 10:26:17 2026 -0700 uniprot otto: an organism whose proteins do not align should not stop the run aquChr2, the golden eagle, took the run down with a bare AssertionError: on assert(os.path.getsize(mapFname)!=0). UniProt has two proteins for taxon 216574 and neither aligned, so the mapping PSL came out empty. That is a fact about the data rather than a failure of the pipeline, and it is the same situation as an organism with no proteins at all, which is already skipped with a warning. Several more taxa in the plan are in the same position, with 1, 2, 8, 13 and 17 proteins. The assembly is now skipped with a message naming it, both when the mapping was just built and when an empty one is being reused from an earlier run. Worker failures also log a traceback now. errAbort raises a bare AssertionError, so the pool reported exactly "taxon 216574 failed: AssertionError:" and said nothing about where, which is worse than what the sequential path used to print. Checked that the errAbort message, the traceback and the file and line all appear. refs #38300 diff --git src/hg/utils/otto/uniprot/doUniprot src/hg/utils/otto/uniprot/doUniprot index c2858fca1a0..96f1d9d05ad 100755 --- src/hg/utils/otto/uniprot/doUniprot +++ src/hg/utils/otto/uniprot/doUniprot @@ -1,20 +1,20 @@ #!/usr/bin/env python3 import urllib.request, urllib.error, urllib.parse, time, os, atexit import datetime, optparse, sys, logging, subprocess, glob, shutil import sys, gzip, logging,re, json -import concurrent.futures +import concurrent.futures, traceback from urllib.parse import urlparse from os.path import * from os import system, makedirs, mkdir, remove, listdir from shutil import move from subprocess import PIPE from collections import defaultdict, namedtuple, Counter, OrderedDict # main driver script for uniprot updates # This script has evolved over a long time. Parts of it do not have an optimal structure. # One reason is that the script used to parse only mutations. Even normal annotations # are still put into a structure that resembles mutations. # the script first uses makeUniProtPsl.sh to create a mapping from UniProt Sequence to genome as a PSL file. @@ -2149,31 +2149,39 @@ stats["minAli"] = MINALI workDir = "clusterRun-map-%s-%s-%s.tmp" % (db, geneTable, fullMd5) scriptArgs = [protFa, transcriptFa, transcriptPsl, str(MINALI), workDir, mapFname] if selectFname is not None: scriptArgs.append(selectFname) # This is where the BLAST alignment-meat happens cmd = ["time", "./makeUniProtPsl.sh"] cmd.extend(scriptArgs) logging.info("Running %s" % cmd) run(cmd) writeMapDesc(stats, db, geneTable, mapDescFname) - assert(os.path.getsize(mapFname)!=0) + if os.path.getsize(mapFname)==0: + # Nothing aligned. On an organism with a handful of UniProt proteins this is a fact + # about the data, not a failure of the run: taxon 216574, the golden eagle, offers + # two proteins and neither of them aligns. Treat it the way an organism with no + # proteins at all is already treated, skip the assembly and carry on, rather than + # asserting and taking the whole run down. refs #38300 + logging.warning("%s: no protein aligned to the genome, skipping this assembly" % db) + return None, geneTable, fullMd5, protMapSource, protToTrans, True + # if you modify this line, also modify the other return statement above return mapFname, geneTable, fullMd5, protMapSource, protToTrans, True def convMapToBigPsl(fastaFname, mapFname, bigPslDir, accToDb, accToMeta, accToMapSource, \ accToTrans, chromSizesFname, doTrembl): " convert the .psl mapping file to bigPsl annotated with a few basic prot infos " bigPslSwissFname = join(bigPslDir, "unipAliSwissprot.new.bb" % locals()) if doTrembl: bigPslTremblFname = join(bigPslDir, "unipAliTrembl.new.bb" % locals()) else: bigPslTremblFname = None protSeqs = parseFasta(open(fastaFname)) infoFname = join(bigPslDir, "protMapInfo.tsv") @@ -2375,30 +2383,35 @@ if onlyDbs is not None and db not in onlyDbs: continue logging.debug("Annotating assembly %s" % db) # a GenArk assembly has no /hive/data/genomes directory, its chrom.sizes # lives in the hub chromSizesFname = chromSizesFile(db) accToMeta = parseRecordMeta(tabDir, taxId, doTrembl) dbMapDir = makeSubDir(mapDir, db) mapDescFname = join(dbMapDir, "liftInfo.json") mapFname, geneTable, mapMd5, accToMapSource, accToTrans, isNewMap = \ makeProteinGenomePsl(taxId, fullFaFname, tabFnames, db, options.force, dbMapDir, mapDescFname) + if mapFname is None or os.path.getsize(mapFname)==0: + # covers both the freshly built and the reused mapping file + logging.warning("%s: no protein/genome alignments, nothing to build" % db) + continue + dbBigBedDir = makeSubDir(bigBedDir, db) convMapToBigPsl(fullFaFname, mapFname, dbBigBedDir, accToDb, accToMeta, accToMapSource, accToTrans, chromSizesFname, doTrembl) #shutil.copy(mapDescFname, mapDir) if options.onlyMap: continue annotFnames = ["tab/swissprot.%d.annots.tab" % taxId] if doTrembl: annotFnames.append( "tab/trembl.%d.annots.tab" % taxId ) uniprotLift(fullFaFname, annotFnames, chromSizesFname, mapFname, dbBigBedDir, accToDb, accToMeta, options) @@ -2430,31 +2443,34 @@ with concurrent.futures.ThreadPoolExecutor(max_workers=options.taxonThreads) as pool: futures = {} for taxId, dbs in todo: fut = pool.submit(oneTaxon, taxId, dbs, onlyDbs, options, tabDir, faDir, mapDir, bigBedDir, doTrembl) futures[fut] = taxId for fut in concurrent.futures.as_completed(futures): taxId = futures[fut] try: fut.result() except BaseException as ex: # BaseException, not Exception: run() reports a failed command with # sys.exit(), which raises SystemExit. In a worker thread that would # otherwise kill just that thread and leave the run looking successful, # which is exactly the kind of silent failure this pipeline has a history of. - logging.error("taxon %s failed: %s: %s" % (taxId, type(ex).__name__, ex)) + # Log the traceback too: errAbort raises a bare AssertionError, so without + # one the message is just "AssertionError:" and says nothing about where. + logging.error("taxon %s failed: %s: %s\n%s" % (taxId, type(ex).__name__, ex, + "".join(traceback.format_exception(type(ex), ex, ex.__traceback__)))) failed.append(taxId) if failed: errAbort("%d of %d taxa failed: %s" % (len(failed), len(todo), ", ".join(str(t) for t in sorted(failed)))) def makeSymlink(target, linkName): assert(".new" not in linkName) # make sure that this bug never happens again if target is None: logging.error("internal error, but not stopping: Symlink to a None target?") return targetPath = abspath(target) if not isfile(targetPath):