Dynamically adding models to sqlalchemy declarative_base

This is a Python tutorial showing how models can dynamically be added to an instance of declarative_base without trigger pep8 unused import. This example works best if the project uses a directory structure similar to the following (__init__.py has been omitted):

  • database
    • database_manager.py
    • models
      • example1.py
      • example2.py

The problem from this structure arises as the same instance of declarative_base needs to be imported by both example1.py and example2.py. While at the same time the database_manager.py would create the database with all its associated tables & relationships. The simplest solution is for the database_manager.py to have the instance declared and all the models to import it. However, this is not possible since all the models need to be imported as well. this is necessary as the database would otherwise be created without the tables & relationships defined by these models. Further complication this construction is that blind imports of the models without calling any method or accessing an attribute would trigger a pep8 unused import.

To resolve this two additional files are defined. The first file allows the definition of a declarative_base in a separate file. While the second file allows for the dynamic importing of models in the models directory. The resulting directory structure looks as follows:

  • database
    • create_database.py
    • database_manager.py
    • declarative_base.py
    • models
      • example1.py
      • example2.py

declarative_base.py

In the declarative_base.py the shared instance of declarative_base is instantiated. Additionally this file can be used to define a base class for all models to inherit automatically.

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative import declared_attr


class Base(object):
    """Base object all sqlalchemy models will extend"""

    @declared_attr
    def __tablename__(cls):
        """Generate tablename based on class name

        Setting the model its __tablename__ attribute will override this
        generated name.
        """
        return cls.__name__.lower()


base = declarative_base(cls=Base)

Let’s cover create_database.py this is the most extensive file as it covers the automatic detection of files and classes inside the models directory. It could be adapted to perform the same or similar automatic detection in various other scenario’s easily. The file contains four methods starting with list_model_names. This method will return a collection of filenames inside the models directory.

import os
import pkgutil

# relative directory for models from this file
models_rel_directory = '/models'

def _list_model_names():
    """Gather collection of model names from models modules

    :return: A collection of module names iterated from the models directory
    """
    model_names = []

    # import everything from models directory relative to this file
    package_path = os.path.dirname(
        os.path.abspath(__file__)) + models_rel_directory

    # iterate over all module files and append their names to the list
    for __, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
        model_names.append(modname)
    return model_names

In list_model_names the path relative to the current file is used to discover the models directory. This directory is subsequently used by pkgutil to find all the modules in this directory. Here every file that is not __init__.py will be added to the collection. Note that at this point the collection contains elements of type str and the actual modules are not yet imported.

The next method is import_models this method will actually import the modules. As argument it takes the collection from list_model_names. In addition it uses the importlib library and works under the assumption the model file names are similar to their class names.

import importlib

model_absolute_path = 'database.models.'

def _import_models(module_names):
    """Gather collection of tuples with modules and associated classes

    :return: A collection of tuples with the module and its associated class
    """
    imported_modules = []

    # for every module names import it and check that it has a class similar
    # to the name of the module. Example: if the file is named account it will
    # look for the Account class. If the file is called account_types it will
    # look for the AccountTypes class.
    for modname in module_names:
        # Capitalize first letters and subsequently remove all underscores.
        class_name = modname.title().replace('_', '')
        mod = importlib.import_module(model_absolute_path + modname)
        if not hasattr(mod, class_name):
            msg = "The module '" + model_absolute_path + ".%s' should have a" \
                  "'%s' class similar to the module name." % \
                  (modname, class_name)
            raise AttributeError(msg)
        else:
            imported_modules.append((mod, class_name))
    return imported_modules

For every str from the passed collection it will try to import it using the specified model_absolute_path. It will automatically determine the class name as long as it follows the pattern as clearly defined in the comment. If the model file is named account_types than the class must be named AccountTypes and so on. Only if the associated class for the module could be found will it be added to the collection. The collection itself consists of tuples with the loaded module and the name of the class.

Now for the method that brings most of these methods together called list_tables. This method will create a collection of all models their __table__ objects. These objects can be passed during the creation of a database when using a declarative_base instance. By accessing these properties and using it in the creation of a database having unused imports is prevented.

def _list_tables():
    """Collection of all the sqlalchemy model __table__ objects

    :return: A Collection of ``__table__`` objects from sqlalchemy models.
    """
    tables = list()
    model_names = _list_model_names()
    imported_modules = _import_models(model_names)
    for module in imported_modules:
        # Access the modules class and subsequent __table__
        tables.append(getattr(module[0], module[1]).__table__)
    return tables

The list_tables method uses getattr to access the module its class by string subsequently accessing the class its __table__ object. This is shown in: tables.append(getattr(module[0], module[1]).__table__).

The final method create_database_tables performs the actual creation of all tables & relationships by using the shared declarative_base. Any type of sqlalchemy engine can be passed to this method and in this example the database_manager.py is expected to call it.

def create_database_tables(engine):
    """Creates the database table using the specified engine"""
    tables = _list_tables()
    base.metadata.create_all(bind=engine, tables=tables)

Finally let’s put these four methods together into one large file.


create_database.py

# -*- encoding: utf-8 -*-
# Copyright (c) 2019 Dantali0n
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import importlib
import os
import pkgutil

from database.declarative_base import base

# relative directory for models from this file
models_rel_directory = '/models'

# absolute path to models for imports
model_absolute_path = 'database.models.'


def create_database_tables(engine):
    """Creates the database table using the specified engine"""
    tables = _list_tables()
    base.metadata.create_all(bind=engine, tables=tables)


def _list_tables():
    """Collection of all the sqlalchemy model __table__ objects

    :return: A Collection of ``__table__`` objects from sqlalchemy models.
    """
    tables = list()
    model_names = _list_model_names()
    imported_modules = _import_models(model_names)
    for module in imported_modules:
        # Access the modules class and subsequent __table__
        tables.append(getattr(module[0], module[1]).__table__)
    return tables


def _list_model_names():
    """Gather collection of model names from models modules

    :return: A collection of module names iterated from the models directory
    """
    model_names = []

    # import everything from models directory relative to this file
    package_path = os.path.dirname(
        os.path.abspath(__file__)) + models_rel_directory

    # iterate over all module files and append their names to the list
    for __, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
        model_names.append(modname)
    return model_names


def _import_models(module_names):
    """Gather collection of tuples with modules and associated classes

    :return: A collection of tuples with the module and its associated class
    """
    imported_modules = []

    # for every module names import it and check that it has a class similar
    # to the name of the module. Example: if the file is named account it will
    # look for the Account class. If the file is called account_types it will
    # look for the AccountTypes class.
    for modname in module_names:
        # Capitalize first letters and subsequently remove all underscores.
        class_name = modname.title().replace('_', '')
        mod = importlib.import_module(model_absolute_path + modname)
        if not hasattr(mod, class_name):
            msg = "The module '" + model_absolute_path + ".%s' should have a" \
                  "'%s' class similar to the module name." % \
                  (modname, class_name)
            raise AttributeError(msg)
        else:
            imported_modules.append((mod, class_name))
    return imported_modules

Leave a Reply

Your email address will not be published. Required fields are marked *

*