Skip to content

Commit

Permalink
Added on_error to Resource/Package
Browse files Browse the repository at this point in the history
  • Loading branch information
roll committed Sep 29, 2020
1 parent e85c491 commit 73a44bf
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 48 deletions.
21 changes: 21 additions & 0 deletions frictionless/package.py
Expand Up @@ -36,6 +36,7 @@ class Package(Metadata):
profile? (str): profile name like 'data-package'
basepath? (str): a basepath of the package
trusted? (bool): don't raise on unsafe paths
on_error? (ignore|warn|raise): behaviour if there is an error
Raises:
FrictionlessException: raise any error that occurs during the process
Expand All @@ -53,6 +54,7 @@ def __init__(
profile=None,
basepath=None,
trusted=None,
on_error="ignore",
):

# Handle zip
Expand All @@ -67,8 +69,15 @@ def __init__(
self.setinitial("profile", profile)
self.__basepath = basepath or helpers.detect_basepath(descriptor)
self.__trusted = trusted
self.__on_error = on_error
super().__init__(descriptor)

def __setattr__(self, name, value):
if name == "on_error":
self.__on_error = value
return
super().__setattr__(name, value)

@Metadata.property
def name(self):
"""
Expand Down Expand Up @@ -109,6 +118,15 @@ def profile(self):
"""
return self.get("profile", config.DEFAULT_PACKAGE_PROFILE)

@property
def on_error(self):
"""
Returns:
ignore|warn|raise: on error bahaviour
"""
assert self.__on_error in ["ignore", "warn", "raise"]
return self.__on_error

# Resources

@Metadata.property
Expand Down Expand Up @@ -422,9 +440,12 @@ def metadata_process(self):
resource,
basepath=self.__basepath,
trusted=self.__trusted,
on_error=self.__on_error,
package=self,
)
list.__setitem__(resources, index, resource)
# NOTE: should we sync basepath/trusted also here?
resource.on_error = self.__on_error
if not isinstance(resources, helpers.ControlledList):
resources = helpers.ControlledList(resources)
resources.__onchange__(self.metadata_process)
Expand Down
49 changes: 34 additions & 15 deletions frictionless/resource.py
Expand Up @@ -42,6 +42,7 @@ class Resource(Metadata):
profile? (str): resource profile
basepath? (str): resource basepath
trusted? (bool): don't raise on unsage paths
on_error? (ignore|warn|raise): behaviour if there is an error
package? (Package): resource package
Raises:
Expand Down Expand Up @@ -69,6 +70,7 @@ def __init__(
profile=None,
basepath=None,
trusted=False,
on_error="ignore",
package=None,
):

Expand All @@ -93,6 +95,7 @@ def __init__(
self.setinitial("profile", profile)
self.__basepath = basepath or helpers.detect_basepath(descriptor)
self.__trusted = trusted
self.__on_error = on_error
self.__package = package
super().__init__(descriptor)

Expand All @@ -101,6 +104,12 @@ def __init__(
if hashing != config.DEFAULT_HASHING:
self["hashing"] = hashing

def __setattr__(self, name, value):
if name == "on_error":
self.__on_error = value
return
super().__setattr__(name, value)

@Metadata.property
def name(self):
"""
Expand Down Expand Up @@ -286,21 +295,6 @@ def compression_path(self):
"""
return self.get("compressionPath")

@Metadata.property
def stats(self):
"""
Returns
dict?: resource stats
"""
stats = {}
for name in ["hash", "bytes", "rows"]:
value = self.get(name)
if value is not None:
if name == "hash":
value = helpers.parse_resource_hash(value)[1]
stats[name] = value
return stats

@Metadata.property
def dialect(self):
"""
Expand Down Expand Up @@ -341,6 +335,30 @@ def profile(self):
"""
return self.get("profile", config.DEFAULT_RESOURCE_PROFILE)

@property
def on_error(self):
"""
Returns:
ignore|warn|raise: on error bahaviour
"""
assert self.__on_error in ["ignore", "warn", "raise"]
return self.__on_error

@Metadata.property
def stats(self):
"""
Returns
dict?: resource stats
"""
stats = {}
for name in ["hash", "bytes", "rows"]:
value = self.get(name)
if value is not None:
if name == "hash":
value = helpers.parse_resource_hash(value)[1]
stats[name] = value
return stats

# Expand

def expand(self):
Expand Down Expand Up @@ -724,6 +742,7 @@ def to_table(self, **options):
options.setdefault("compression_path", self.compression_path)
options.setdefault("dialect", self.dialect)
options.setdefault("schema", self.schema)
options.setdefault("on_error", self.__on_error)
if "lookup" not in options:
options["lookup"] = self.read_lookup()
return Table(**options)
Expand Down
35 changes: 30 additions & 5 deletions frictionless/table.py
Expand Up @@ -107,7 +107,7 @@ class Table:
It defaults to `['']`
on_error? (ignore|warn|raise): Define behaviour if there is an error in the
header or rows during a `table.read_rows` call.
header or rows during the reading rows process.
It defaults to `ignore`.
lookup? (dict): The lookup is a special object providing relational information.
Expand Down Expand Up @@ -147,9 +147,6 @@ def __init__(
lookup=None,
):

# Validate arguments
assert on_error in ["ignore", "warn", "raise"]

# Update source
if isinstance(source, Path):
source = str(source)
Expand Down Expand Up @@ -191,6 +188,9 @@ def __init__(
self.__on_error = on_error
self.__lookup = lookup

# Set error handler
self.on_error = on_error

# Create file
self.__file = File(
source=source,
Expand All @@ -205,6 +205,12 @@ def __init__(
query=query,
)

def __setattr__(self, name, value):
if name == "on_error":
self.__on_error = value
return
super().__setattr__(name, value)

def __enter__(self):
if self.closed:
self.open()
Expand Down Expand Up @@ -313,6 +319,24 @@ def schema(self):
"""
return self.__schema

@property
def on_error(self):
"""
Returns:
ignore|warn|raise: on error bahaviour
"""
assert self.__on_error in ["ignore", "warn", "raise"]
return self.__on_error

# TODO: use property wrapper to make it shorter
@on_error.setter
def on_error(self, value):
"""
Parameters:
value (ignore|warn|raise): on error bahaviour
"""
self.__on_error = value

@property
def header(self):
"""
Expand Down Expand Up @@ -415,7 +439,8 @@ def closed(self):
"""
return self.__parser is None

# Read
# Read
return self.__on_error

def read_data(self):
"""Read data stream into memory
Expand Down
73 changes: 63 additions & 10 deletions tests/test_package.py
Expand Up @@ -2,7 +2,7 @@
import json
import zipfile
import pytest
from frictionless import Package, exceptions
from frictionless import Package, Resource, exceptions


# General
Expand Down Expand Up @@ -500,7 +500,60 @@ def test_package_compression_explicit_zip():

# Integrity

INTEGRITY_DESCRIPTOR = {

def test_resource_integrity_on_error():
package = Package(resources=[Resource(path="data/invalid.csv")])
resource = package.resources[0]
assert package.on_error == "ignore"
assert resource.on_error == "ignore"
assert resource.read_rows()


def test_resource_integrity_on_error_header_warn():
data = [["name"], [1], [2], [3]]
schema = {"fields": [{"name": "bad"}]}
package = Package(resources=[Resource(data=data, schema=schema)], on_error="warn")
resource = package.resources[0]
assert package.on_error == "warn"
assert resource.on_error == "warn"
with pytest.warns(UserWarning):
resource.read_rows()


def test_resource_integrity_on_error_header_raise():
data = [["name"], [1], [2], [3]]
schema = {"fields": [{"name": "bad"}]}
package = Package({"resources": [{"data": data, "schema": schema}]}, on_error="raise")
resource = package.resources[0]
assert package.on_error == "raise"
assert resource.on_error == "raise"
with pytest.raises(exceptions.FrictionlessException):
resource.read_rows()


def test_resource_integrity_on_error_row_warn():
data = [["name"], [1], [2], [3]]
schema = {"fields": [{"type": "string"}]}
package = Package(resources=[Resource(data=data, schema=schema)], on_error="warn")
resource = package.resources[0]
assert package.on_error == "warn"
assert resource.on_error == "warn"
with pytest.warns(UserWarning):
resource.read_rows()


def test_resource_integrity_on_error_row_raise():
data = [["name"], [1], [2], [3]]
schema = {"fields": [{"type": "string"}]}
package = Package({"resources": [{"data": data, "schema": schema}]}, on_error="raise")
resource = package.resources[0]
assert package.on_error == "raise"
assert resource.on_error == "raise"
with pytest.raises(exceptions.FrictionlessException):
resource.read_rows()


DESCRIPTOR_FK = {
"resources": [
{
"name": "main",
Expand Down Expand Up @@ -538,8 +591,8 @@ def test_package_compression_explicit_zip():
}


def test_package_integrity():
package = Package(INTEGRITY_DESCRIPTOR)
def test_package_integrity_foreign_key():
package = Package(DESCRIPTOR_FK)
resource = package.get_resource("main")
rows = resource.read_rows()
assert rows[0].valid
Expand All @@ -553,7 +606,7 @@ def test_package_integrity():


def test_package_integrity_foreign_key_invalid():
package = Package(INTEGRITY_DESCRIPTOR)
package = Package(DESCRIPTOR_FK)
package.resources[1].data[3][0] = "bad"
resource = package.get_resource("main")
rows = resource.read_rows()
Expand All @@ -568,7 +621,7 @@ def test_package_integrity_foreign_key_invalid():


def test_package_integrity_foreign_key_self_reference():
package = Package(INTEGRITY_DESCRIPTOR)
package = Package(DESCRIPTOR_FK)
package.resources[0].schema.foreign_keys = [
{"fields": "parent_id", "reference": {"resource": "", "fields": "id"}}
]
Expand All @@ -580,7 +633,7 @@ def test_package_integrity_foreign_key_self_reference():


def test_package_integrity_foreign_key_self_reference_invalid():
package = Package(INTEGRITY_DESCRIPTOR)
package = Package(DESCRIPTOR_FK)
package.resources[0].data[2][0] = "0"
package.resources[0].schema.foreign_keys = [
{"fields": "parent_id", "reference": {"resource": "", "fields": "id"}}
Expand All @@ -593,7 +646,7 @@ def test_package_integrity_foreign_key_self_reference_invalid():


def test_package_integrity_foreign_key_multifield():
package = Package(INTEGRITY_DESCRIPTOR)
package = Package(DESCRIPTOR_FK)
package.resources[0].schema.foreign_keys = [
{
"fields": ["name", "surname"],
Expand All @@ -608,7 +661,7 @@ def test_package_integrity_foreign_key_multifield():


def test_package_integrity_foreign_key_multifield_invalid():
package = Package(INTEGRITY_DESCRIPTOR)
package = Package(DESCRIPTOR_FK)
package.resources[0].schema.foreign_keys = [
{
"fields": ["name", "surname"],
Expand All @@ -624,7 +677,7 @@ def test_package_integrity_foreign_key_multifield_invalid():


def test_package_integrity_read_lookup():
package = Package(INTEGRITY_DESCRIPTOR)
package = Package(DESCRIPTOR_FK)
resource = package.get_resource("main")
lookup = resource.read_lookup()
assert lookup == {"people": {("firstname",): {("Walter",), ("Alex",), ("John",)}}}
Expand Down

0 comments on commit 73a44bf

Please sign in to comment.