Repository: frictionlessdata/frictionless-py

Issue:
Resource names coming out of 'infer' are not unique
# Overview

I have a test data set that has a nested directory structure (`/test` and `/train`). There are files under each directory with the same basename, and that results in resources with non-unique names. See excerpt here:

```json
    {
      "path": "scolianni/mnistasjpg/sample/test/img_130.jpg",
      "name": "img_130",
      "profile": "data-resource",
      "scheme": "file",
      "format": "jpg",
      "hashing": "md5",
      "encoding": "utf-8",
      "compression": "no",
      "compressionPath": "",
      "control": {},
      "stats": {
        "hash": "09d2a818efbf97cabd7d73069f8eb30c",
        "bytes": 637,
        "fields": 0,
        "rows": 0
      }
    },
    {
      "path": "scolianni/mnistasjpg/sample/train/4/img_130.jpg",
      "name": "img_130",
      "profile": "data-resource",
      "scheme": "file",
      "format": "jpg",
      "hashing": "md5",
      "encoding": "utf-8",
      "compression": "no",
      "compressionPath": "",
      "control": {},
      "stats": {
        "hash": "09d2a818efbf97cabd7d73069f8eb30c",
        "bytes": 637,
        "fields": 0,
        "rows": 0
      }
    }
```

By my reading of the Data Resource spec, this is not acceptable. I also realized that using just the basename of a file for its name also yields non-unique resource names if there are two files with the same name but different extensions (an example might be a binary file and its accompanying index). My current solution is to extend each resource's name with 8 characters of the md5 hash of its path and format (concatenated). 

Roughly:

```python
res.name = res.name + '_' + hashlib.md5(
    res.path.encode('utf-8') + res.format.encode('utf-8')
    ).hexdigest()[:8]
```

This yields:

```json
    {
      "path": "scolianni/mnistasjpg/sample/test/img_130.jpg",
      "name": "img_130_06b95777",
      "profile": "data-resource",
      "scheme": "file",
      "format": "jpg",
      "hashing": "md5",
      "encoding": "utf-8",
      "compression": "no",
      "compressionPath": "",
      "control": {},
      "stats": {
        "hash": "09d2a818efbf97cabd7d73069f8eb30c",
        "bytes": 637,
        "fields": 0,
        "rows": 0
      }
    },
    {
      "path": "scolianni/mnistasjpg/sample/train/4/img_130.jpg",
      "name": "img_130_9209f717",
      "profile": "data-resource",
      "scheme": "file",
      "format": "jpg",
      "hashing": "md5",
      "encoding": "utf-8",
      "compression": "no",
      "compressionPath": "",
      "control": {},
      "stats": {
        "hash": "09d2a818efbf97cabd7d73069f8eb30c",
        "bytes": 637,
        "fields": 0,
        "rows": 0
      }
    }
```
---

Please preserve this line to notify @roll (lead of this repository)


Discussion:

Patch:
diff --git a/frictionless/package.py b/frictionless/package.py
index c3dac90d..d53b9b3e 100644
--- a/frictionless/package.py
+++ b/frictionless/package.py
@@ -119,7 +119,7 @@ class Package(Metadata):
         """
         return self.get("profile", config.DEFAULT_PACKAGE_PROFILE)
 
-    @Metadata.property(write=False)
+    @Metadata.property(cache=False, write=False)
     def hashing(self):
         """
         Returns:
@@ -127,7 +127,7 @@ class Package(Metadata):
         """
         return self.__hashing
 
-    @Metadata.property(write=False)
+    @Metadata.property(cache=False, write=False)
     def basepath(self):
         """
         Returns:
@@ -135,16 +135,15 @@ class Package(Metadata):
         """
         return self.__basepath
 
-    @property
+    @Metadata.property(cache=False, write=False)
     def onerror(self):
         """
         Returns:
             ignore|warn|raise: on error bahaviour
         """
-        assert self.__onerror in ["ignore", "warn", "raise"]
         return self.__onerror
 
-    @Metadata.property(write=False)
+    @Metadata.property(cache=False, write=False)
     def trusted(self):
         """
         Returns:
@@ -163,7 +162,7 @@ class Package(Metadata):
         resources = self.get("resources", [])
         return self.metadata_attach("resources", resources)
 
-    @Metadata.property(write=False)
+    @Metadata.property(cache=False, write=False)
     def resource_names(self):
         """
         Returns:
@@ -273,6 +272,15 @@ class Package(Metadata):
         for resource in self.resources:
             resource.infer(only_sample=only_sample)
 
+        # Deduplicate names
+        if len(self.resource_names) != len(set(self.resource_names)):
+            seen_names = []
+            for index, name in enumerate(self.resource_names):
+                count = seen_names.count(name) + 1
+                if count > 1:
+                    self.resources[index].name = "%s%s" % (name, count)
+                seen_names.append(name)
+
     # Import/Export
 
     @staticmethod
diff --git a/frictionless/resource.py b/frictionless/resource.py
index 2c01e8d7..e14e435b 100644
--- a/frictionless/resource.py
+++ b/frictionless/resource.py
@@ -313,7 +313,7 @@ class Resource(Metadata):
         stats = {"hash": "", "bytes": 0, "fields": 0, "rows": 0}
         return self.metadata_attach("stats", self.get("stats", stats))
 
-    @Metadata.property(write=False)
+    @Metadata.property(cache=False, write=False)
     def basepath(self):
         """
         Returns
@@ -321,7 +321,7 @@ class Resource(Metadata):
         """
         return self.__basepath
 
-    @Metadata.property(write=False)
+    @Metadata.property(cache=False, write=False)
     def fullpath(self):
         """
         Returns
@@ -333,16 +333,15 @@ class Resource(Metadata):
             return "multipart"
         return self.source
 
-    @property
+    @Metadata.property(cache=False, write=False)
     def onerror(self):
         """
         Returns:
             ignore|warn|raise: on error bahaviour
         """
-        assert self.__onerror in ["ignore", "warn", "raise"]
         return self.__onerror
 
-    @property
+    @Metadata.property(cache=False, write=False)
     def trusted(self):
         """
         Returns:
@@ -350,7 +349,7 @@ class Resource(Metadata):
         """
         return self.__trusted
 
-    @property
+    @Metadata.property(cache=False, write=False)
     def package(self):
         """
         Returns:
diff --git a/frictionless/schema.py b/frictionless/schema.py
index 813471ed..76cabc79 100644
--- a/frictionless/schema.py
+++ b/frictionless/schema.py
@@ -92,7 +92,7 @@ class Schema(Metadata):
         fields = self.get("fields", [])
         return self.metadata_attach("fields", fields)
 
-    @Metadata.property(write=False)
+    @Metadata.property(cache=False, write=False)
     def field_names(self):
         """
         Returns:


Test patch:
diff --git a/tests/test_package.py b/tests/test_package.py
index 5f84c0b2..a5316887 100644
--- a/tests/test_package.py
+++ b/tests/test_package.py
@@ -429,6 +429,20 @@ def test_package_infer_empty_file():
     assert package.resources[0].stats["bytes"] == 0
 
 
+def test_package_infer_duplicate_resource_names_issue_530():
+    package = Package(
+        resources=[
+            Resource(path="data/chunk1.csv"),
+            Resource(path="data/chunk2.csv"),
+            Resource(path="data/tables/chunk1.csv"),
+            Resource(path="data/tables/chunk2.csv"),
+        ]
+    )
+    package.infer()
+    assert len(set(package.resource_names)) == 4
+    assert package.resource_names == ["chunk1", "chunk2", "chunk12", "chunk22"]
+
+
 # Import/Export