Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions gramps/cli/arghandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,43 @@ def _split_options(options_str):
return options_str_dict


def _choose_import_plugin(plugins, extension, filename):
"""
Return the best import plugin for *filename* among *plugins*.

Plugins that declare a ``sniff_function`` are tested first; the first one
whose sniff function returns ``True`` for *filename* is returned. If none
matches, the first plugin whose extension equals *extension* and that has
no sniff function is returned as a fallback. Returns ``None`` when no
suitable plugin is found.

This allows multiple importers to share the same file extension while each
handling a distinct file version (e.g. GEDCOM 5.5 vs GEDCOM 7.0).

:param plugins: list of :class:`~gramps.gen.plug.ImportPlugin` instances
:type plugins: list
:param extension: lower-case file extension without leading dot
:type extension: str
:param filename: full path of the file to import
:type filename: str
:returns: the chosen plugin, or ``None``
:rtype: :class:`~gramps.gen.plug.ImportPlugin` | None
"""
candidates = [p for p in plugins if extension == p.get_extension()]
for plugin in candidates:
sniff = plugin.get_sniff_function()
if sniff is not None:
try:
if sniff(filename):
return plugin
except Exception: # pylint: disable=broad-except
pass
for plugin in candidates:
if plugin.get_sniff_function() is None:
return plugin
return None


# -------------------------------------------------------------------------
# ArgHandler
# -------------------------------------------------------------------------
Expand Down Expand Up @@ -595,10 +632,11 @@ def cl_import(self, filename, family_tree_format):
Try to import filename using the family_tree_format.
"""
pmgr = BasePluginManager.get_instance()
for plugin in pmgr.get_import_plugins():
if family_tree_format == plugin.get_extension():
import_function = plugin.get_import_function()
import_function(self.dbstate.db, filename, self.user)
plugin = _choose_import_plugin(
pmgr.get_import_plugins(), family_tree_format, filename
)
if plugin is not None:
plugin.get_import_function()(self.dbstate.db, filename, self.user)

# -------------------------------------------------------------------------
#
Expand Down
80 changes: 60 additions & 20 deletions gramps/gen/plug/_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,52 +21,92 @@
This module provides the :class:`.Plugin` class for import plugins.
"""

# ------------------------
# Python modules
# ------------------------
from collections.abc import Callable

# ------------------------
# Gramps modules
# ------------------------
from . import Plugin


# ------------------------------------------------------------
#
# ImportPlugin
#
# ------------------------------------------------------------
class ImportPlugin(Plugin):
"""
This class represents a plugin for importing data into Gramps
This class represents a plugin for importing data into Gramps.
"""

def __init__(self, name, description, import_function, extension):
def __init__(
self,
name: str,
description: str,
import_function: Callable,
extension: str,
sniff_function: Callable | None = None,
) -> None:
"""
Initialise the ImportPlugin.

:param name: A friendly name to call this plugin.
Example: "GEDCOM Import"
:type name: string
:type name: str
:param description: A short description of the plugin.
Example: "This plugin will import a GEDCOM file into a database"
:type description: string
:type description: str
:param import_function: A function to call to perform the import.
The function must take the form:
def import_function(db, filename, user):
where:
"db" is a Gramps database to import the data into
"filename" is the file that contains data to be imported
"user" is an instance of the User class implementing
GUI functions (callbacks, errors, warnings, etc)
The function must take the form ``import_function(db, filename,
user)`` where *db* is a Gramps database, *filename* is the file
to import, and *user* is a :class:`gramps.gen.user.User` instance.
:type import_function: callable
:param extension: The extension for the files imported by this plugin.
Example: "ged"
:param extension: The file extension handled by this plugin (without
the leading dot). Example: ``"ged"``
:type extension: str
:return: nothing
:param sniff_function: Optional callable that accepts a filename and
returns ``True`` when this plugin should handle that file. Used to
disambiguate between importers that share the same extension (e.g.
GEDCOM 5.5 vs GEDCOM 7.0). When ``None`` (the default) the plugin
acts as a fallback for its extension.
:type sniff_function: callable | None
"""
Plugin.__init__(self, name, description, import_function.__module__)
self.__import_func = import_function
self.__extension = extension
self.__sniff_func = sniff_function

def get_import_function(self):
def get_import_function(self) -> Callable:
"""
Get the import function for this plugins.
Return the import function for this plugin.

:return: the callable import_function passed into :meth:`__init__`
:returns: the callable import_function passed into :meth:`__init__`
:rtype: callable
"""
return self.__import_func

def get_extension(self):
def get_extension(self) -> str:
"""
Get the extension for the files imported by this plugin.
Return the file extension handled by this plugin.

:return: str
:returns: file extension string (without leading dot)
:rtype: str
"""
return self.__extension

def get_sniff_function(self) -> Callable | None:
"""
Return the sniff function for this plugin, or ``None`` if not set.

The sniff function accepts a filename and returns ``True`` when this
plugin is the correct handler for the file. It is called when multiple
importers are registered for the same extension so that the right one
can be selected based on file content rather than extension alone.

:returns: the sniff callable, or ``None``
:rtype: callable | None
"""
return self.__sniff_func
4 changes: 4 additions & 0 deletions gramps/gen/plug/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,11 +557,15 @@ def get_import_plugins(self):
continue
mod = self.load_plugin(pdata)
if mod:
sniff_func = None
if pdata.sniff_function:
sniff_func = getattr(mod, pdata.sniff_function, None)
imp = ImportPlugin(
name=pdata.name,
description=pdata.description,
import_function=getattr(mod, pdata.import_function),
extension=pdata.extension,
sniff_function=sniff_func,
)
self.__import_plugins.append(imp)

Expand Down
31 changes: 31 additions & 0 deletions gramps/gen/plug/_pluginreg.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ class PluginData:
.. attribute:: import_function
Function that starts an import

.. attribute:: sniff_function
Optional function that returns True if the plugin can handle the given
file, used to distinguish between multiple importers for the same
extension (e.g. GEDCOM 5.5 vs GEDCOM 7.0).

Attributes for GRAMPLET plugins

.. attribute:: gramplet
Expand Down Expand Up @@ -507,6 +512,7 @@ def __init__(self):
self._export_options_title = ""
# IMPORT attr
self._import_function = None
self._sniff_function = None
# GRAMPLET attr
self._gramplet = None
self._height = 200
Expand Down Expand Up @@ -1000,6 +1006,31 @@ def import_function(self, import_function):
raise ValueError("import_function may only be set for IMPORT plugins")
self._import_function = import_function

@property
def sniff_function(self):
"""
Return the name of the sniff function for this import plugin.

:returns: The name of the sniff function, or ``None`` if not set.
:rtype: str | None
"""
return self._sniff_function

@sniff_function.setter
def sniff_function(self, sniff_function: str) -> None:
"""
Set the name of the sniff function for this import plugin.

:param sniff_function: The name of a callable in the plugin module
that accepts a filename and returns ``True`` if this plugin
should handle the file. Used when multiple importers share the
same extension (e.g. GEDCOM 5.5 vs GEDCOM 7.0).
:type sniff_function: str
"""
if self._ptype != IMPORT:
raise ValueError("sniff_function may only be set for IMPORT plugins")
self._sniff_function = sniff_function

# GRAMPLET attributes
@property
def gramplet(self):
Expand Down
Loading