Skip to content

TypeError: Can't instantiate abstract class in post_migrate_treenode for proxy models using Python ABCs #215

@simensol

Description

@simensol

Description

Python version: 3.14

Django version: 6.0

Package version: 0.23.3

Current behavior (bug description)

The post_migrate_treenode signal handler calls update_tree() on models that are Python-level abstract classes (using abc.ABC / @abstractmethod), even though they are registered as concrete Django models (e.g. proxy models). This causes a TypeError because Python cannot instantiate an abstract class.

Error:

TypeError: Can't instantiate abstract class AbstractBaseFeideFetchTask without an implementation for abstract method '...'

Stack trace:

File ".venv/lib/python3.14/site-packages/treenode/signals.py", line 33, in post_migrate_treenode
    sender_model.update_tree()
  ...
TypeError: Can't instantiate abstract class AbstractBaseFeideFetchTask without an implementation for abstract method '...'

Root cause:

In treenode/signals.py, __is_treenode_model checks isclass() and issubclass(), but does not check inspect.isabstract():

def __is_treenode_model(sender):
    from .models import TreeNodeModel
    return (
        isclass(sender)
        and issubclass(sender, TreeNodeModel)
        and sender != TreeNodeModel
    )

This allows abstract proxy models through. Django's get_models() returns all registered models including proxy models, and a proxy model can be a Python ABC if it uses a metaclass combining ABCMeta and Django's ModelBase, and declares @abstractmethod methods. Such models pass the isclass and issubclass checks but cannot be instantiated — so calling update_tree() on them fails.

Note: This is a different issue from #207, which fixed the RuntimeError: dictionary changed size during iteration bug. The list() fix in v0.23.3 solved that problem, but this abstract class issue remains.

Reproduction steps

  1. Create a Django model inheriting from a TreeNodeModel subclass using a metaclass that combines ABCMeta with Django's ModelBase:
from abc import ABCMeta, abstractmethod
from django.db.models.base import ModelBase
from treenode.models import TreeNodeModel

class ABCMetaModelBase(ABCMeta, ModelBase):
    """Metaclass combining ABCMeta and Django's ModelBase."""
    pass

class ConcreteTreeNodeModel(TreeNodeModel):
    """A concrete TreeNode model."""
    class Meta:
        app_label = "myapp"

class AbstractProxyTask(ConcreteTreeNodeModel, metaclass=ABCMetaModelBase):
    """A proxy model that is also a Python ABC — registered in Django but not instantiable."""

    class Meta:
        proxy = True

    @abstractmethod
    def my_method(self):
        """Subclasses must implement this."""
        ...
  1. Run migrations:
python manage.py migrate
  1. The post_migrate_treenode signal fires and calls update_tree() on AbstractProxyTask, which raises:
TypeError: Can't instantiate abstract class AbstractProxyTask without an implementation for abstract method 'my_method'

Expected behavior

The post_migrate_treenode signal handler should skip models that are Python abstract classes (inspect.isabstract() returns True), since they cannot be instantiated and therefore update_tree() will fail.

Proposed fix

Add an inspect.isabstract() check in __is_treenode_model:

from inspect import isclass, isabstract

def __is_treenode_model(sender):
    from .models import TreeNodeModel
    return (
        isclass(sender)
        and not isabstract(sender)
        and issubclass(sender, TreeNodeModel)
        and sender != TreeNodeModel
    )

This skips any model that Python considers abstract (i.e., has unimplemented @abstractmethod methods), preventing the TypeError.

Workaround

Until this is fixed, users can monkey-patch the signal handler in their AppConfig.ready():

import inspect

def _fixed_post_migrate_treenode(sender, **kwargs):
    for sender_model in list(sender.get_models()):
        if (
            hasattr(sender_model, "update_tree")
            and not inspect.isabstract(sender_model)
        ):
            sender_model.update_tree()

class MyAppConfig(AppConfig):
    def ready(self):
        from django.db import models as django_models
        from treenode import signals as treenode_signals

        django_models.signals.post_migrate.disconnect(
            receiver=treenode_signals.post_migrate_treenode,
            dispatch_uid="post_migrate_treenode",
        )
        django_models.signals.post_migrate.connect(
            receiver=_fixed_post_migrate_treenode,
            dispatch_uid="post_migrate_treenode",
        )

Impact

This bug affects projects that:

  • Use django-treenode with proxy models that are also Python ABCs (using ABCMeta + @abstractmethod)
  • Run migrations (both in production and during test setup)

The fix is minimal (adding isabstract()) and should have no performance impact.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions