Repository: frictionlessdata/frictionless-py

Issue:
Ability to set Detector properties
# Overview

See https://github.com/frictionlessdata/frictionless-py/issues/1014

Currently, it's not possible to set Detector properties after an instance is created:

```python
detector = Detector(buffer_size=100)
detector.buffer_size = 200 # wont work
```

We want to make it settable probably using an approach we use for other classes in Frictionless (for example, for the Field class):

```python
    def __setattr__(self, name, value):
        if name == "schema":
            self.__schema = value
        else:
            return super().__setattr__(name, value)
```

The list of properties we want to be able to set (all of the current):
- buffer_size
- sample_size
- encoding_function
- encoding_confidence
- field_type
- field_names,
- field_confidence
- field_float_numbers
- field_missing_values
- schema_sync
- schema_patch

# Plan
- [ ] implement property setter using `__setattr__` for Detector 
- [ ] write a few tests for the new functionality in `tests/test_detector.py`
- [ ] create a PR and request a review from @roll

Discussion:
roll: @shashigharti Yea you might mistakenly have used the same value for testing whether we can set a different value. 

No worries it just needs a quick fix 
shashigharti: Hi @aivuk 

It is because It is testing two aspects one is:

- passing params during initialization
- setting private properties using setters.

I think I am using same values so I will change it to different value for second one.
roll: > In your setter logic you could have used the standard Python setattr method. Instead of a lot of if/else you can have something:
> 
> `setattr(self, '_{}'.format(name), value)`

@aivuk will it work for private attributes?
shashigharti: That will not work. I tried it yesterday. 

> In your setter logic you could have used the standard Python setattr method. Instead of a lot of if/else you can have something:
> 
> `setattr(self, '_{}'.format(name), value)`

This may not work as I tried it yesterday. So I shifted to if else.
shashigharti: > @shashigharti Yea you might mistakenly have used the same value for testing whether we can set a different value.
> 
> No worries it just needs a quick fix

Yes. I will fix and push the new changes.
aivuk: > > In your setter logic you could have used the standard Python setattr method. Instead of a lot of if/else you can have something:
> > `setattr(self, '_{}'.format(name), value)`
> 
> This may not work as I tried it yesterday. So I shifted to if else.

Why it didn't work? 
aivuk: > > > In your setter logic you could have used the standard Python setattr method. Instead of a lot of if/else you can have something:
> > > `setattr(self, '_{}'.format(name), value)`
> > 
> > 
> > This may not work as I tried it yesterday. So I shifted to if else.
> 
> Why it didn't work?

Ok, recursion problem, Got it. 
shashigharti: @aivuk @roll 
Thank you for the suggestion, I have made the changes according to your suggestion.

Patch:
diff --git a/docs/guides/framework/detector-guide.md b/docs/guides/framework/detector-guide.md
index fa75849a..a645c33f 100644
--- a/docs/guides/framework/detector-guide.md
+++ b/docs/guides/framework/detector-guide.md
@@ -65,6 +65,24 @@ detector = Detector(sample_size=1000)
 resource = describe("data/country-1.csv", detector=detector)
 ```
 
+
+### Encoding Function
+
+By default, Frictionless encoding_function is None and user can use built in encoding functions. But user has option to implement their own encoding using this feature. The following example simply returns utf-8 encoding but user can add more complex logic to the encoding function.
+
+```python script title="Python"
+from frictionless import Detector
+from frictionless import Resource
+
+detector = Detector(encoding_function=lambda sample: "utf-8")
+with Resource("data/table.csv", detector=detector) as resource:
+  assert resource.encoding == "utf-8"
+  assert resource.sample == [["id", "name"], ["1", "english"], ["2", "中国人"]]
+  assert resource.fragment == [["1", "english"], ["2", "中国人"]]
+  assert resource.header == ["id", "name"]
+```
+
+
 ### Field Type
 
 This option allows manually setting all the field types to a given type. It's useful when you need to skip data casting (setting `any` type) or have everything as a string (setting `string` type):
diff --git a/frictionless/detector.py b/frictionless/detector.py
index f5d3a6cf..ecd66095 100644
--- a/frictionless/detector.py
+++ b/frictionless/detector.py
@@ -27,9 +27,13 @@ class Detector:
         buffer_size? (int): The amount of bytes to be extracted as a buffer.
             It defaults to 10000
 
-        sample_size? (int): The amount of rows to be extracted as a samle.
+        sample_size? (int): The amount of rows to be extracted as a sample.
             It defaults to 100
 
+        encoding_function? (func): A custom encoding function for the file.
+
+        encoding_confidence? (float): Confidence value for encoding function.
+
         field_type? (str): Enforce all the inferred types to be this type.
             For more information, please check "Describing  Data" guide.
 
@@ -89,6 +93,33 @@ class Detector:
         self.__schema_sync = schema_sync
         self.__schema_patch = schema_patch
 
+    def __setattr__(self, name, value):
+        if name == "buffer_size":
+            self.__buffer_size = value
+        elif name == "sample_size":
+            self.__sample_size = value
+        elif name == "field_type":
+            self.__field_type = value
+        elif name == "encoding_function":
+            self.__encoding_function = value
+        elif name == "encoding_confidence":
+            self.__encoding_confidence = value
+        elif name == "field_names":
+            self.__field_names = value
+        elif name == "field_confidence":
+            self.__field_confidence = value
+        elif name == "field_float_numbers":
+            self.__field_float_numbers = value
+        elif name == "field_missing_values":
+            self.__field_missing_values = value
+        elif name == "schema_sync":
+            self.__schema_sync = value
+        elif name == "schema_patch":
+            self.__schema_patch = value
+        # other properties
+        else:
+            return super().__setattr__(name, value)
+
     @property
     def buffer_size(self):
         return self.__buffer_size
@@ -97,6 +128,42 @@ class Detector:
     def sample_size(self):
         return self.__sample_size
 
+    @property
+    def encoding_function(self):
+        return self.__encoding_function
+
+    @property
+    def encoding_confidence(self):
+        return self.__encoding_confidence
+
+    @property
+    def field_type(self):
+        return self.__field_type
+
+    @property
+    def field_names(self):
+        return self.__field_names
+
+    @property
+    def field_confidence(self):
+        return self.__field_confidence
+
+    @property
+    def field_float_numbers(self):
+        return self.__field_float_numbers
+
+    @property
+    def field_missing_values(self):
+        return self.__field_missing_values
+
+    @property
+    def schema_sync(self):
+        return self.__schema_sync
+
+    @property
+    def schema_patch(self):
+        return self.__schema_patch
+
     # Detect
 
     def detect_encoding(self, buffer, *, encoding=None):
@@ -109,7 +176,7 @@ class Detector:
             str: encoding
         """
 
-        # Use defined
+        # User defined
         if self.__encoding_function:
             return self.__encoding_function(buffer)
 


Test patch:
diff --git a/tests/test_detector.py b/tests/test_detector.py
index c24829c2..fee9b833 100644
--- a/tests/test_detector.py
+++ b/tests/test_detector.py
@@ -68,3 +68,81 @@ def test_schema_infer_no_names():
     assert schema == {
         "fields": [{"name": "field1", "type": "integer"}],
     }
+
+
+def test_detector_set_buffer_size():
+    detector = Detector(buffer_size=10)
+    assert detector.buffer_size == 10
+    detector.buffer_size = 20
+    assert detector.buffer_size == 20
+
+
+def test_detector_set_sample_size():
+    detector = Detector(sample_size=1000)
+    assert detector.sample_size == 1000
+    detector.sample_size = 800
+    assert detector.sample_size == 800
+
+
+def test_detector_set_encoding_function():
+    enc_func = lambda sample: "utf-8"
+    detector = Detector(encoding_function=enc_func)
+    assert detector.encoding_function == enc_func
+    detector.encoding_function = enc_func
+    assert detector.encoding_function == enc_func
+
+
+def test_detector_set_encoding_confidence():
+    detector = Detector(encoding_confidence=0.9)
+    assert detector.encoding_confidence == 0.9
+    detector.encoding_confidence = 0.8
+    assert detector.encoding_confidence == 0.8
+
+
+def test_detector_set_field_type():
+    detector = Detector(field_type="string")
+    assert detector.field_type == "string"
+    detector.field_type = "int"
+    assert detector.field_type == "int"
+
+
+def test_detector_set_field_names():
+    detector = Detector(field_names=["f1", "f2", "f3"])
+    assert detector.field_names == ["f1", "f2", "f3"]
+    detector.field_names = ["f1", "f2"]
+    assert detector.field_names == ["f1", "f2"]
+
+
+def test_detector_set_field_confidence():
+    detector = Detector(field_confidence=0.8)
+    assert detector.field_confidence == 0.8
+    detector.field_confidence = 0.9
+    assert detector.field_confidence == 0.9
+
+
+def test_detector_set_field_float_numbers():
+    detector = Detector(field_float_numbers=True)
+    assert detector.field_float_numbers is True
+    detector.field_float_numbers = False
+    assert detector.field_float_numbers is False
+
+
+def test_detector_set_field_missing_values():
+    detector = Detector(field_missing_values=["", "67"])
+    assert detector.field_missing_values == ["", "67"]
+    detector.field_missing_values = ["", "70"]
+    assert detector.field_missing_values == ["", "70"]
+
+
+def test_detector_set_schema_sync():
+    detector = Detector(schema_sync=True)
+    assert detector.schema_sync is True
+    detector.schema_sync = False
+    assert detector.schema_sync is False
+
+
+def test_detector_set_schema_patch():
+    detector = Detector(schema_patch={"fields": {"id": {"type": "string"}}})
+    assert detector.schema_patch == {"fields": {"id": {"type": "string"}}}
+    detector.schema_patch = {"fields": {"age": {"type": "int"}}}
+    assert detector.schema_patch == {"fields": {"age": {"type": "int"}}}