Source code for pestifer.molecule.segment

#Author: Cameron F. Abrams, <cfa22@drexel.edu>
"""
Defines the Segment class for generating and managing segments declared for psfgen
"""
from typing import ClassVar, List, TYPE_CHECKING
from pydantic import Field
import logging

logger=logging.getLogger(__name__)

from .chainidmanager import ChainIDManager
from .residue import Residue, ResidueList
from .stateinterval import StateIntervalList

from ..core.baseobj import BaseObj, BaseObjList
from ..core.objmanager import ObjManager

from ..objs.cleavagesite import CleavageSite
from ..objs.deletion import DeletionList
from ..objs.graft import GraftList
from ..objs.link import LinkList
from ..objs.mutation import MutationList
from ..objs.patch import PatchList
from ..objs.resid import ResID
from ..objs.ssbond import SSBondList

from ..psfutil.psfcontents import PSFSegmentList

from ..util.stringthings import my_logger

if TYPE_CHECKING:
    from ..molecule.molecule import Molecule

[docs] class Segment(BaseObj): """ This class represents a segment defined by a list of residue indices and provides methods to check if a bond intersects the segment. It also provides methods to yield treadmilled versions of the segment and to check for equality with another segment. A segment represents a set of residues in which there are not repeated resids. """ _required_fields = {'segtype', 'segname', 'chainID', 'residues', 'subsegments', 'parent_chain', 'specs'} _optional_fields = {'mutations', 'deletions', 'grafts', 'patches', 'attachments', 'psfgen_segname', 'objmanager', 'parent_molecule'} segtype: str = Field(..., description="The type of the segment (e.g., 'protein', 'nucleicacid', 'glycan').") segname: str = Field(..., description="The name of the segment as it is understood by psfgen and in a PSF file.") chainID: str = Field(..., description="The chain ID associated with the segment.") residues: ResidueList = Field(default_factory=ResidueList, description="A list of residues that make up the segment.") subsegments: StateIntervalList = Field(..., description="A list of subsegments within the segment, each with a state indicating whether it is resolved or missing.") parent_chain: str = Field(..., description="The chain ID of the parent chain to which this segment belongs.") specs: dict = Field(..., description="Specifications for the segment, including details like loops and terminal modifications.") mutations: MutationList | None = Field(default=None, description="A list of mutations applied to the segment.") deletions: DeletionList | None = Field(default=None, description="A list of deletions applied to the segment.") grafts: GraftList | None = Field(default=None, description="A list of grafts applied to the segment.") patches: PatchList | None = Field(default=None, description="A list of patches applied to the segment.") attachments: List | None = Field(default=None, description="A list of attachments applied to the segment (Attachments are currently unimplemented).") psfgen_segname: str | None = Field(default=None, description="The segment name as understood by psfgen.") objmanager: ObjManager = Field(default_factory=ObjManager, description="The object manager for the segment, used to track and manage objects associated with the segment.") parent_molecule: 'Molecule' = Field(default=None, description="The parent molecule to which this segment belongs. This is set after the segment is constructed.") _inheritable_objs: ClassVar[set] = {'mutations', 'patches', 'Cfusions', 'grafts'} """ List of inheritable modifications for the segment. These modifications include mutations, patches, C-terminal fusions, and grafts. """ @classmethod def _adapt(cls, *args, **kwargs) -> dict: if args: segname_override = kwargs.pop('segname', 'UNSET') specs = kwargs.pop('specs', {}) if isinstance(args[0], Residue): residue = args[0] apparent_chainID = residue.chainID apparent_segtype = residue.segtype apparent_segname = residue.chainID if segname_override == 'UNSET' else segname_override myRes = ResidueList([]) myRes.append(residue) subsegments = myRes.state_bounds(lambda x: 'RESOLVED' if len(x.atoms) > 0 else 'MISSING') input_dict = { 'specs': specs, 'segtype': apparent_segtype, 'segname': apparent_segname, 'chainID': apparent_chainID, 'residues': myRes, 'subsegments': subsegments, 'parent_chain': apparent_chainID } return input_dict elif isinstance(args[0], ResidueList): residues: ResidueList = args[0] apparent_chainID = residues.data[0].chainID apparent_segtype = residues.data[0].segtype apparent_segname = residues.data[0].segname if segname_override == 'UNSET' else segname_override if apparent_segtype in ['protein', 'nucleicacid']: # a protein segment must have unique residue numbers assert residues.puniq(['resid']), f'ChainID {apparent_chainID} has duplicate resid!' # a protein segment may not have more than one protein chain assert all([x.chainID==residues.data[0].chainID for x in residues.data]) residues.sort() # for r in residues: # logger.debug(f'Residue {r.resname}{r.resid.resid} with {len(r.atoms)} atoms (resolved: {r.resolved})') subsegments = residues.state_bounds(lambda x: 'RESOLVED' if x.resolved else 'MISSING') # logger.debug(f'Segment {apparent_segname} has {len(residues)} residues across {len(subsegments)} subsegments') else: skip_uniquify = kwargs.get('skip_uniquify', False) if not skip_uniquify: logger.debug(f'Calling puniquify on {len(residues)} residues of non-polymer segment {apparent_segname} by attribute \'resid\'') residues.puniquify(attrs=['resid']) logger.debug(f'counting affected residues...') count = sum([1 for x in residues.data if len(x.atoms)>0 and 'resid' in x.atoms.data[0].ORIGINAL_ATTRIBUTES and x.resid != x.atoms.data[0].ORIGINAL_ATTRIBUTES['resid']]) if count > 0: logger.debug(f'{count} residue(s) were affected by puniquify:') for x in residues.data: if len(x.atoms) > 0 and len(x.atoms.data[0].ORIGINAL_ATTRIBUTES) > 0: logger.debug(f' {x.chainID} {x.resname} {x.resid.resid} was resid {x.atoms.data[0].ORIGINAL_ATTRIBUTES["resid"].resid}') else: logger.debug(f'No duplicate resids found in {len(residues)} residues of non-polymer segment {apparent_segname}') subsegments = residues.state_bounds(lambda x: 'RESOLVED' if len(x.atoms)>0 else 'MISSING') logger.debug(f'Segment {apparent_segname} has {len(residues)} residues across {len(subsegments)} subsegments') input_dict = { 'specs': specs, 'segtype': apparent_segtype, 'segname': apparent_segname, 'chainID': apparent_chainID, 'residues': residues, 'subsegments': subsegments, 'parent_chain': apparent_chainID } return input_dict else: # generate an input_dict for an empty, unconstructed segment # built from kwargs input_dict = { 'specs': kwargs.get('specs', {}), 'segtype': kwargs.get('segtype', 'unconstructed'), 'segname': kwargs.get('segname', 'unconstructed'), 'chainID': kwargs.get('chainID', 'unconstructed'), 'residues': kwargs.get('residues', ResidueList([])), 'subsegments': kwargs.get('subsegments', StateIntervalList([])), 'parent_chain': kwargs.get('parent_chain', 'unconstructed') } return input_dict return {} def __str__(self): return f'{self.segname}: type {self.segtype} chain {self.chainID} with {len(self.residues)} residues'
[docs] def set_parent_molecule(self, parent_molecule): """ Set the parent molecule for this segment. Parameters ---------- parent_molecule : object The parent molecule to which this segment belongs. """ self.parent_molecule = parent_molecule
[docs] def cleave(self, clv:CleavageSite, daughter_chainID): """ Cleave the segment at a specified cut location, creating a daughter segment. Parameters ---------- clv : Cleavage The cut location specifying where to cleave the segment. daughter_chainID : str The chain ID to assign to the daughter segment. """ assert clv.chainID == self.segname assert self.segtype == 'protein' r2i = self.residues.iget(lambda x: x.resid == clv.resid2 and x.chainID == clv.chainID) # These two slice operations create *copies* of lists of residues # The original list of residues contains elements that are referenced # by other structures, like links. assert isinstance(self.residues, ResidueList) daughter_residues = self.residues[r2i:] assert isinstance(daughter_residues, ResidueList) assert daughter_residues[0].resid == clv.resid2 parent_residues = self.residues[:r2i] assert daughter_residues[0] is self.residues[r2i] self.residues = parent_residues # this set_chainID call must act DEEPLY on structures in the list items # that have chainID attributes AND we have to revisit all links! daughter_residues.set(chainID=daughter_chainID) Dseg = Segment(daughter_residues, segname=daughter_chainID) assert isinstance(Dseg, Segment) Dseg.objmanager = self.objmanager.expel(daughter_residues) Dseg.set_parent_molecule(self.parent_molecule) return Dseg
[docs] class SegmentList(BaseObjList[Segment]): """ A list of segments in a molecular structure. This class represents a collection of segments and provides methods to manage and manipulate them. It inherits from `AncestorAwareObjList` to maintain the context of the parent molecule. """ def __init__(self, data): super().__init__(data) self.segnames = [] self.counters_by_segtype = {} self.daughters = {} self.glycan_segment_parents: dict[str, str] = {} self.segtype_of_segname = {} self.segtypes_ordered = [] self.seq_spec = {} self.residues: ResidueList = None self.chainIDmanager: ChainIDManager = None self.psfcompanion: PSFSegmentList = None self.links = None
[docs] def describe(self) -> str: return f'<SegmentList with {len(self)} segments>'
[docs] def generate_from_residues(self, seq_spec: dict = {}, residues: ResidueList = [], chainIDmanager: ChainIDManager = None, psfcompanion: PSFSegmentList = None, links=None): """ Generate a SegmentList from a sequence specification and a list of residues extracted from a structure file. Parameters ---------- seq_spec : dict A dictionary containing sequence specifications. residues : ResidueList A list of residues to be processed into segments. chainIDmanager : ChainIDManager An object managing chain IDs to ensure uniqueness. psfcompanion: PSFSegmentList A companion PSF segment list to provide additional context. Returns ------- SegmentList A new SegmentList instance containing the generated segments. """ if not all([x.segtype != 'UNSET' for x in residues.data]): raise ValueError(f'There are residues with UNSET segtype: {[(x.resname, x.chainID, x.resid.resid) for x in residues.data if x.segtype == "UNSET"]}') self.residues = residues self.seq_spec = seq_spec self.chainIDmanager = chainIDmanager self.links = links if psfcompanion is None: return self.build_from_only_pdb_data() else: self.psfcompanion = psfcompanion self.build_from_psf_and_pdb_data()
def _build_glycan_trees(self, all_glycan_residues: list) -> list[dict]: """Find connected glycan trees using residue identity, then sort by protein attachment. Returns a list of dicts sorted by (parent_chainID, parent_resid): { 'residues': [Residue, ...], # BFS-ordered from protein-attached root 'parent_chainID': str, 'parent_resid': ResID, 'root_residue': Residue | None, # glycan residue directly bonded to protein } """ glycan_ids: dict[int, object] = {id(r): r for r in all_glycan_residues} adj: dict[int, list[int]] = {id(r): [] for r in all_glycan_residues} protein_anchor: dict[int, tuple] = {} # glycan node id → (protein_chainID, protein_resid) if self.links: for link in self.links: r1, r2 = link.residue1, link.residue2 if r1 is None or r2 is None: continue r1_glycan = id(r1) in glycan_ids r2_glycan = id(r2) in glycan_ids if r1_glycan and r2_glycan: adj[id(r1)].append(id(r2)) adj[id(r2)].append(id(r1)) elif r1_glycan and getattr(r2, 'segtype', '') == 'protein': protein_anchor[id(r1)] = (r2.chainID, r2.resid) elif r2_glycan and getattr(r1, 'segtype', '') == 'protein': protein_anchor[id(r2)] = (r1.chainID, r1.resid) def _bfs_from(start_id: int) -> list: result, seen, q = [], {start_id}, [start_id] while q: curr = q.pop(0) result.append(glycan_ids[curr]) for nb in adj[curr]: if nb not in seen: seen.add(nb) q.append(nb) return result visited: set[int] = set() trees: list[dict] = [] for r in all_glycan_residues: if id(r) in visited: continue # first BFS to collect the component from an arbitrary start component = _bfs_from(id(r)) for c in component: visited.add(id(c)) # find the protein-anchored root in this component root_residue = None parent_chainID = None parent_resid = None for c in component: if id(c) in protein_anchor: root_residue = c parent_chainID, parent_resid = protein_anchor[id(c)] break # re-order from the root so residues are always root-outward if root_residue is not None: component = _bfs_from(id(root_residue)) else: parent_chainID = component[0].chainID parent_resid = component[0].resid trees.append({ 'residues': component, 'parent_chainID': parent_chainID, 'parent_resid': parent_resid, 'root_residue': root_residue, }) trees.sort(key=lambda t: (t['parent_chainID'], t['parent_resid'].resseqnum)) return trees
[docs] def build_from_only_pdb_data(self): """ Build the segment list given residues that were populated only from PDB data """ for r in self.residues.data: if not r.chainID in self.segtype_of_segname: self.segtype_of_segname[r.chainID] = r.segtype if not r.segtype in self.segtypes_ordered: self.segtypes_ordered.append(r.segtype) logger.debug(f'Generating segments from list of {len(self.residues)} residues. segtypes detected: {self.segtypes_ordered}') initial_chainIDs = list(self.segtype_of_segname.keys()) logger.debug(f'ChainIDs detected: {initial_chainIDs}') self.chainIDmanager.sandbag(initial_chainIDs) for stype in self.segtypes_ordered: self.counters_by_segtype[stype] = 0 res = self.residues.filter(lambda x: x.segtype == stype) logger.debug(f'Processing {len(res)} residues of segtype {stype} (out of {len(self.residues)})') if stype == 'glycan' and not self.seq_spec.get('skip_glycan_renaming', False): glycan_cfg = self.seq_spec.get('glycans', {}) max_glycan_size = glycan_cfg.get('max_glycan_size', 30) numbering = glycan_cfg.get('numbering', 'narrow') wide_shift = glycan_cfg.get('wide_shift', 3000) trees = self._build_glycan_trees(list(res.data)) logger.debug(f'Found {len(trees)} glycan trees; numbering={numbering}') # For "narrow": compute max protein resid per chain so glycan blocks start above it if numbering == 'narrow': protein_max_resid: dict[str, int] = {} for r in self.residues.data: if r.segtype == 'protein': v = r.resid.resseqnum if r.chainID not in protein_max_resid or v > protein_max_resid[r.chainID]: protein_max_resid[r.chainID] = v # For "wide": detect inter-glycan resid collisions on the same chain. # Collision is possible when two glycosylation sites are closer together # than the size of the larger glycan. Note: increasing wide_shift does # NOT help since the shift cancels out in the difference. if numbering == 'wide': wide_ranges: dict[str, list[tuple[int, int, int]]] = {} # chainID → [(base, end, parent_resid), ...] for tree in trees: pc = tree['parent_chainID'] base = tree['parent_resid'].resseqnum + wide_shift end = base + len(tree['residues']) - 1 for prev_base, prev_end, prev_pr in wide_ranges.get(pc, []): if base <= prev_end: logger.warning( f'Wide glycan numbering collision on chain {pc}: ' f'glycan at protein resid {tree["parent_resid"].resseqnum} ' f'(resids {base}-{end}) overlaps glycan at protein resid {prev_pr} ' f'(resids {prev_base}-{prev_end}). ' f'Consider using numbering: narrow or increasing separation between glycosylation sites.' ) wide_ranges.setdefault(pc, []).append((base, end, tree['parent_resid'].resseqnum)) glycan_local_ctr: dict[str, int] = {} for tree in trees: parent_chainID = tree['parent_chainID'] component = tree['residues'] if numbering == 'as-is': # Segname is keyed off the glycan's own original chainID so it # stays unique; resids and chainIDs on atoms are left untouched. orig_chainID = component[0].chainID glycan_local_ctr[orig_chainID] = glycan_local_ctr.get(orig_chainID, 0) + 1 k = glycan_local_ctr[orig_chainID] segname = f'{orig_chainID}G{k:02d}' else: glycan_local_ctr[parent_chainID] = glycan_local_ctr.get(parent_chainID, 0) + 1 k = glycan_local_ctr[parent_chainID] segname = f'{parent_chainID}G{k:02d}' if numbering == 'wide': base_resid = tree['parent_resid'].resseqnum + wide_shift else: # narrow parent_max = protein_max_resid.get(parent_chainID, 0) base_resid = parent_max + (k - 1) * max_glycan_size + 1 for i, r in enumerate(component): new_resid = ResID(base_resid + i) r.resid = new_resid for a in r.atoms.data: a.resid = new_resid r.set_chainID(parent_chainID) c_res = ResidueList(component) self.segtype_of_segname[segname] = stype self.glycan_segment_parents[segname] = parent_chainID num_mis = sum(1 for x in component if len(x.atoms) == 0) thisSeg = Segment(c_res, segname=segname, specs=self.seq_spec) resid_range = 'as-is' if numbering == 'as-is' else f'{base_resid}-{base_resid+len(component)-1}' logger.debug(f'Made glycan segment: parent_chain {parent_chainID} segname {segname} k={k} resids {resid_range} ({num_mis} missing)') self.append(thisSeg) self.segnames.append(segname) self.counters_by_segtype[stype] += 1 continue orig_chainIDs = res.uniqattrs(['chainID'])['chainID'] logger.debug(f'Found {len(orig_chainIDs)} unique chainIDs for segtype {stype}: {orig_chainIDs}') orig_res_groups = {chainID: res.filter(lambda x: x.chainID == chainID) for chainID in orig_chainIDs} for chainID, c_res in orig_res_groups.items(): assert isinstance(c_res, ResidueList), f'ChainID {chainID} residues are not a ResidueList: {type(c_res)}' logger.debug(f'-> original chainID {chainID} in {len(c_res)} residues') this_chainID = chainID this_chainID = self.chainIDmanager.check(this_chainID) if this_chainID != chainID: logger.debug(f'{len(c_res)} residues with original chainID {chainID} and segtype {stype} are assigned new daughter chainID {this_chainID}') if not chainID in self.daughters: self.daughters[chainID] = [] self.daughters[chainID].append(this_chainID) c_res.set_chainIDs(this_chainID) self.segtype_of_segname[this_chainID] = stype if stype == 'protein': if chainID in self.seq_spec['build_zero_occupancy_C_termini']: logger.debug(f'-> appending new chainID {this_chainID} to build_zero_occupancy_C_termini due to member {chainID}') self.seq_spec['build_zero_occupancy_C_termini'].insert(self.seq_spec['build_zero_occupancy_C_termini'].index(chainID), this_chainID) if chainID in self.seq_spec['build_zero_occupancy_N_termini']: logger.debug(f'-> appending new chainID {this_chainID} to build_zero_occupancy_N_termini due to member {chainID}') self.seq_spec['build_zero_occupancy_N_termini'].insert(self.seq_spec['build_zero_occupancy_N_termini'].index(chainID), this_chainID) num_mis = sum([1 for x in c_res if len(x.atoms) == 0]) thisSeg = Segment(c_res, segname=this_chainID, specs=self.seq_spec) logger.debug(f'Made segment: stype {stype} chainID {this_chainID} segname {thisSeg.segname} ({num_mis} missing)') my_logger(self.seq_spec, logger.debug) self.append(thisSeg) self.segnames.append(thisSeg.segname) self.counters_by_segtype[stype] += 1 return self
[docs] def build_from_psf_and_pdb_data(self): """ Build the segment list using the pre-populated PSF segment list and PDB-derived residues """ if not self.psfcompanion.num_residues() == len(self.residues.data): logger.debug(f'Number of residues in PSF: {self.psfcompanion.num_residues()}') logger.debug(f'Number of residues in working molecule: {len(self.residues.data)}') if not self.psfcompanion.num_atoms() == sum(len(x.atoms) for x in self.residues.data): logger.debug(f'Number of atoms in PSF: {self.psfcompanion.num_atoms()}') logger.debug(f'Number of atoms in working molecule: {sum(len(x.atoms) for x in self.residues.data)}') # for each segment in the psfcompanion, build a list of actual residues extracted from self.residues for which atom serials match those in the PSFResidues for seg in self.psfcompanion.data: matching_residues = ResidueList(list(filter(lambda x: x.segname == seg.segname, self.residues.data))) if not matching_residues: raise ValueError(f'No matching residues found for segment {seg.segname}') logger.debug(f'For segment {seg.segname} in PSF, found {len(matching_residues)} matching residues in PDB data, from resid {matching_residues.data[0].resid.resid} to resid {matching_residues.data[-1].resid.resid}') chainIDs = list(set([x.chainID for x in matching_residues.data])) assert len(chainIDs) == 1, f'Multiple chainIDs found for segment {seg.segname}: {chainIDs}' my_chainID = chainIDs[0] """ Since we are building from a complete psf/pdb set, there should be no chainID collisions, but each segment should represent only one chainID. """ self.chainIDmanager.touch(my_chainID) self.append(Segment(matching_residues, segname=seg.segname, specs=self.seq_spec, skip_uniquify=True))
[docs] def get_residue_to_segname_map(self) -> dict: """Return a mapping from id(residue) → segname for all residues in all segments.""" result = {} for S in self.data: for r in S.residues.data: result[id(r)] = S.segname return result
[docs] def collect_residues(self): """ Collect all residues from the segments in the list. Returns ------- ResidueList A list of all residues associated with the segments.""" residues = ResidueList([]) for seg in self.data: residues.extend(seg.residues) return residues
[docs] def get_segment_of_residue(self, residue: Residue) -> Segment | None: """ Get the segment that contains a specified residue. Parameters ---------- residue : Residue The residue for which to find the containing segment. Returns ------- Segment or None The segment containing the specified residue, or None if not found. """ logger.debug(f'Looking for {residue.chainID}_{residue.resid.resid} in all segments {", ".join([S.segname for S in self.data])}') for S in self.data: logger.debug(f' -> {residue.chainID}_{residue.resid.resid} in segment {S.segname}') if any(r is residue for r in S.residues): logger.debug(f' found it!') return S raise ValueError(f'Residue {residue.chainID}_{residue.resid.resid} not found in any segment!')
[docs] def remove(self, item): """ Remove a segment from the list and update associated attributes. Parameters ---------- item : Segment The segment to remove from the list. """ self.segnames.remove(item.segname) self.counters_by_segtype[self.segtype_of_segname[item.segname]] -= 1 return super().remove(item)
[docs] def prune_topology(self, mutations: MutationList, links: LinkList, ssbonds: SSBondList): """ Prunes links, ssbonds, and even whole segments based on mutations """ pruned_objects = {'residues': ResidueList([]), 'links': LinkList([]), 'ssbonds': SSBondList([]), 'segments': SegmentList([])} bad_ssbonds = SSBondList([]) for ssbond in ssbonds.data: mutation1 = mutations.get(lambda x: x.chainID == ssbond.chainID1 and x.resid == ssbond.resid1) mutation2 = mutations.get(lambda x: x.chainID == ssbond.chainID2 and x.resid == ssbond.resid2) logger.debug(f'Checking disulfide bond {ssbond} for mutations...') logger.debug(f' mutation1: {mutation1}, mutation2: {mutation2}') if mutation1 and mutation1.newresname != 'CYS': bad_ssbonds.append(ssbond) elif mutation2 and mutation2.newresname != 'CYS': bad_ssbonds.append(ssbond) for ssbond in bad_ssbonds.data: ssbonds.remove(ssbond) pruned_objects['ssbonds'].append(ssbond) bad_links = LinkList([]) for link in links.data: mutation1 = mutations.get(lambda x: x.chainID == link.chainID1 and x.resid == link.resid1) mutation2 = mutations.get(lambda x: x.chainID == link.chainID2 and x.resid == link.resid2) if mutation1 or mutation2: bad_links.append(link) if len(bad_links) > 0: Alllin = LinkList([]) for link in bad_links.data: links.remove(link) pruned_objects['links'].append(link) res, lin = link.residue2.get_down_group() to_delete = ResidueList([link.residue2] + list(res)) Alllin.extend(lin) if to_delete: logger.debug(f'Deleting residues down from and including {str(to_delete[0])} due to a mutation') S = self.get_segment_of_residue(to_delete[0]) logger.debug(f'Segment {S.segname} contains residues that must be deleted because they are downstream (right) of a deleted link.') for r in to_delete: logger.debug(f'...{str(r)}') S.residues.remove_instance(r) pruned_objects['residues'].append(r) if len(S.residues) == 0: logger.debug(f'All residues of {S.segname} are deleted; {S.segname} is deleted') self.remove(S) pruned_objects['segments'].append(S) if Alllin: for l in Alllin: links.remove(l) pruned_objects['links'].append(l) return pruned_objects
[docs] def inherit_objs(self, objmanager: ObjManager): """ Inherit objects from the object manager for each segment in the list. This method updates the object manager for each segment with inherited objects such as mutations, grafts, and patches. Parameters ---------- objmanager : ObjectManager The object manager from which to inherit objects. """ for item in self.data: item.objmanager = objmanager.filter_copy(lambda x: x.chainID == item.segname, objnames=item._inheritable_objs) counts = item.objmanager.counts_by_header() logger.debug(f'Segment {item.segname} inherits:') my_logger(counts, logger.debug, depth=1)