Repository: frictionlessdata/frictionless-py

Issue:
Consider more flexible field initialization
The `frictionless.Schema` class ignores a list of [anything other than `dict`](https://github.com/frictionlessdata/frictionless-py/blob/bffe82989f0fc782cb51b0c83bf2dee521e55ad8/frictionless/schema.py#L287) for the `fields` keyword. Would you consider accepting any instance of `collections.abc.MutableMapping`? This would not be a breaking change because `isinstance(dict(), collections.abc.MutableMapping)` equals `true`.

My use case: I'm constructing the field metadata in a `pandas.DataFrame` and using the `into` keyword of `pandas.DataFrame.to_dict` to customize the mapping produced. My custom output subclasses `collections.abc.MutableMapping`, but is not an instance of `dict`.



Discussion:

Patch:
diff --git a/frictionless/file.py b/frictionless/file.py
index 849b99b1..3de349ec 100644
--- a/frictionless/file.py
+++ b/frictionless/file.py
@@ -1,5 +1,6 @@
 import os
 import glob
+from collections.abc import Mapping
 from pathlib import Path
 from .helpers import cached_property
 from . import settings
@@ -148,7 +149,7 @@ class File:
         # Detect type
         type = "table"
         if not multipart:
-            if memory and isinstance(data, dict):
+            if memory and isinstance(data, Mapping):
                 type = "resource"
                 if data.get("fields") is not None:
                     type = "schema"
diff --git a/frictionless/metadata.py b/frictionless/metadata.py
index b6854bd2..daaf3ea6 100644
--- a/frictionless/metadata.py
+++ b/frictionless/metadata.py
@@ -3,6 +3,7 @@ import json
 import yaml
 import jsonschema
 import stringcase
+from collections.abc import Mapping
 from pathlib import Path
 from operator import setitem
 from functools import partial
@@ -189,7 +190,7 @@ class Metadata(helpers.ControlledDict):
         try:
             if descriptor is None:
                 return {}
-            if isinstance(descriptor, dict):
+            if isinstance(descriptor, Mapping):
                 if not self.metadata_duplicate:
                     return descriptor
                 try:
@@ -279,7 +280,7 @@ class Metadata(helpers.ControlledDict):
 
 def metadata_to_dict(value):
     process = lambda value: value.to_dict() if hasattr(value, "to_dict") else value
-    if isinstance(value, dict):
+    if isinstance(value, Mapping):
         value = {key: metadata_to_dict(process(value)) for key, value in value.items()}
     elif isinstance(value, list):
         value = [metadata_to_dict(process(value)) for value in value]


Test patch:
diff --git a/tests/test_package.py b/tests/test_package.py
index eee22166..0551dc5d 100644
--- a/tests/test_package.py
+++ b/tests/test_package.py
@@ -3,6 +3,7 @@ import json
 import yaml
 import pytest
 import zipfile
+from collections.abc import Mapping
 from pathlib import Path
 from frictionless import Package, Resource, Layout, describe_package, helpers
 from frictionless import FrictionlessException
@@ -34,6 +35,28 @@ def test_package_from_dict():
     }
 
 
+class NotADict(Mapping):
+    def __init__(self, **kwargs):
+        self.__dict__.update(**kwargs)
+
+    def __getitem__(self, key):
+        return self.__dict__[key]
+
+    def __iter__(self):
+        return iter(self.__dict__)
+
+    def __len__(self):
+        return len(self.__dict__)
+
+
+def test_package_from_mapping():
+    package = Package(NotADict(name="name", profile="data-package"))
+    assert package == {
+        "name": "name",
+        "profile": "data-package",
+    }
+
+
 def test_package_from_path():
     package = Package("data/package.json")
     assert package.name == "name"