Repository: frictionlessdata/frictionless-py

Issue:
Custom types registration
There exists a need in several cases to register custom types

In the same way that GoodTables allows registration of custom checks.

Currently, it seems that the only solution is monkey-patching or have I missed something in the docs?

Discussion:

Patch:
diff --git a/frictionless/field.py b/frictionless/field.py
index f8a3df19..750c566d 100644
--- a/frictionless/field.py
+++ b/frictionless/field.py
@@ -1,12 +1,12 @@
 import re
 import decimal
 import warnings
-import importlib
 from copy import copy
 from operator import setitem
 from functools import partial
 from collections import OrderedDict
 from .metadata import Metadata
+from .system import system
 from . import errors
 from . import config
 
@@ -71,7 +71,7 @@ class Field(Metadata):
         Returns:
             str: name
         """
-        return self.get("name", "field")
+        return self.get("name", self.type)
 
     @Metadata.property
     def title(self):
@@ -320,10 +320,7 @@ class Field(Metadata):
     def metadata_process(self):
 
         # Type
-        type = self.get("type", "any")
-        name = f"{type.capitalize()}Type"
-        module = importlib.import_module("frictionless.types")
-        self.__type = getattr(module, name, getattr(module, "AnyType"))(self)
+        self.__type = system.create_type(self)
 
     def metadata_validate(self):
         yield from super().metadata_validate()
diff --git a/frictionless/system.py b/frictionless/system.py
index 2cddc6d5..bdd2370a 100644
--- a/frictionless/system.py
+++ b/frictionless/system.py
@@ -9,8 +9,10 @@ from .dialect import Dialect
 from . import errors
 
 
-# NOTE: Consider plugins priority
-# NOTE: Consider an ability to register plugins dynamically
+# TODO: Add plugin.name?
+# TODO: Rework system.register
+# TODO: Consider plugins priority
+# TODO: Use lists instead of ordered dicts?
 class System:
     """System representation
 
@@ -23,6 +25,21 @@ class System:
 
     """
 
+    def __init__(self):
+        self.__dynamic_plugins = OrderedDict()
+
+    def register(self, name, plugin):
+        """Register a plugin
+
+        Parameters:
+            name (str): plugin name
+            plugin (Plugin): plugin to register
+        """
+        self.__dynamic_plugins[name] = plugin
+        if "methods" in self.__dict__:
+            del self.__dict__["plugins"]
+            del self.__dict__["methods"]
+
     # Actions
 
     actions = [
@@ -34,6 +51,7 @@ class System:
         "create_pipeline",
         "create_server",
         "create_storage",
+        "create_type",
     ]
 
     def create_check(self, name, *, descriptor=None):
@@ -202,6 +220,27 @@ class System:
             raise FrictionlessException(errors.Error(note=note))
         return storage
 
+    def create_type(self, field):
+        """Create checks
+
+        Parameters:
+            field (Field): corresponding field
+
+        Returns:
+            Type: type
+        """
+        name = field.type
+        types = import_module("frictionless.types")
+        for func in self.methods["create_type"].values():
+            type = func(field)
+            if type is not None:
+                return type
+        Class = getattr(types, f"{name.capitalize()}Type", getattr(types, "AnyType"))
+        #  if Class is None:
+        #  note = f'cannot create type "{name}". Try installing "frictionless-{name}"'
+        #  raise FrictionlessException(errors.FieldError(note=note))
+        return Class(field)
+
     # Methods
 
     @cached_property
@@ -228,7 +267,7 @@ class System:
         for _, name, _ in pkgutil.iter_modules([os.path.dirname(module.__file__)]):
             module = import_module(f"frictionless.plugins.{name}")
             modules[name] = module
-        plugins = OrderedDict()
+        plugins = OrderedDict(self.__dynamic_plugins)
         for name, module in modules.items():
             Plugin = getattr(module, f"{name.capitalize()}Plugin", None)
             if Plugin:


Test patch:
diff --git a/tests/test_type.py b/tests/test_type.py
new file mode 100644
index 00000000..b797a9f5
--- /dev/null
+++ b/tests/test_type.py
@@ -0,0 +1,24 @@
+from frictionless import system, Plugin, Type, Resource, Schema, Field
+
+
+def test_type_custom():
+
+    # Type
+    class CustomType(Type):
+        def read_cell(self, cell):
+            return [cell]
+
+    # Plugin
+    class CustomPlugin(Plugin):
+        def create_type(self, field):
+            if field.type == "custom":
+                return CustomType(field)
+
+    # Testing
+    system.register("custom", CustomPlugin())
+    schema = Schema(fields=[Field(type="integer"), Field(type="custom")])
+    resource = Resource(path="data/table.csv", schema=schema)
+    assert resource.read_rows() == [
+        {"integer": 1, "custom": ["english"]},
+        {"integer": 2, "custom": ["中国人"]},
+    ]