def populate_generic_lookup_table(
        sa_class: GenericLookupClassType,
        datadir: str,
        session: Session,
        replace: bool = False,
        commit: bool = True,
        commitevery: int = DEFAULT_COMMIT_EVERY) -> None:
    """
    Populates one of many generic lookup tables with ONSPD data.

    We find the data filename from the ``__filename__`` property of the
    specific class, hunting for it within ``datadir`` and its subdirectories.

    The ``.TXT`` files look at first glance like tab-separated values files,
    but in some cases have inconsistent numbers of tabs (e.g. "2011 Census
    Output Area Classification Names and Codes UK.txt"). So we'll use the
    ``.XLSX`` files.

    If the headings parameter is passed, those headings are used. Otherwise,
    the first row is used for headings.

    Args:
        sa_class: SQLAlchemy ORM class
        datadir: root directory of ONSPD data
        session: SQLAlchemy ORM database session
        replace: replace tables even if they exist? (Otherwise, skip existing
            tables.)
        commit: COMMIT the session once we've inserted the data?
        commitevery: if committing: commit every *n* rows inserted
    """
    tablename = sa_class.__tablename__
    filename = find_first(sa_class.__filename__, datadir)
    headings = getattr(sa_class, '__headings__', [])
    debug = getattr(sa_class, '__debug_content__', False)
    n = 0

    if not replace:
        engine = session.bind
        if engine.has_table(tablename):
            log.info(f"Table {tablename} exists; skipping")
            return

    log.info(f"Dropping/recreating table: {tablename}")
    sa_class.__table__.drop(checkfirst=True)
    sa_class.__table__.create(checkfirst=True)

    log.info(f'Processing file "{filename}" -> table "{tablename}"')
    ext = os.path.splitext(filename)[1].lower()
    type_xlsx = ext in ['.xlsx']
    type_csv = ext in ['.csv']
    file = None  # type: Optional[TextIO]

    def dict_from_rows(row_iterator: Iterable[List]) \
            -> Generator[Dict, None, None]:
        local_headings = headings
        first_row = True
        for row in row_iterator:
            values = values_from_row(row)
            if first_row and not local_headings:
                local_headings = values
            else:
                yield dict(zip(local_headings, values))
            first_row = False

    if type_xlsx:
        workbook = openpyxl.load_workbook(filename)  # read_only=True
        # openpyxl BUG: with read_only=True, cells can have None as their value
        # when they're fine if opened in non-read-only mode.
        # May be related to this:
        # https://bitbucket.org/openpyxl/openpyxl/issues/601/read_only-cell-row-column-attributes-are  # noqa
        sheet = workbook.active
        dict_iterator = dict_from_rows(sheet.iter_rows())
    elif type_csv:
        file = open(filename, 'r')
        csv_reader = csv.DictReader(file)
        dict_iterator = csv_reader
    else:
        raise ValueError("Only XLSX and CSV these days")
        # workbook = xlrd.open_workbook(filename)
        # sheet = workbook.sheet_by_index(0)
        # dict_iterator = dict_from_rows(sheet.get_rows())
    for datadict in dict_iterator:
        n += 1
        if debug:
            log.critical(f"{n}: {datadict}")
        # filter out blanks:
        datadict = {k: v for k, v in datadict.items() if k}
        # noinspection PyNoneFunctionAssignment
        obj = sa_class(**datadict)
        session.add(obj)
        if commit and n % commitevery == 0:
            commit_and_announce(session)
    if commit:
        commit_and_announce(session)
    log.info(f"... inserted {n} rows")

    if file:
        file.close()