Repository: frictionlessdata/frictionless-py

Issue:
Validation does not work with stream resources
Followup to #737 

Example:

```python
from frictionless import Resource, validate_resource

with open("test.csv", "w") as f1:
    f1.write("id,name\n1,german\n2,english")

with open('test.csv', 'rb') as f2:
  resource = Resource(f2, format='csv')
  print(validate_resource(resource))
```

Gives:

```python
{'version': '4.0.11', 'time': 0.01, 'errors': [{'code': 'task-error', 'name': 'Task Error', 'tags': ['#general'], 'note': 'read of closed file', 'message': 'The task has an error: read of closed file', 'description': 'General task-level error.'}], 'tasks': [], 'stats': {'errors': 1, 'tasks': 0}, 'valid': False}
```

Discussion:

Patch:
diff --git a/docs/tutorials/schemes/buffer-tutorial.md b/docs/tutorials/schemes/buffer-tutorial.md
index f6d106a2..d5eac5bf 100644
--- a/docs/tutorials/schemes/buffer-tutorial.md
+++ b/docs/tutorials/schemes/buffer-tutorial.md
@@ -3,41 +3,35 @@ title: Buffer Tutorial
 sidebar_label: Buffer
 ---
 
-Frictionless supports loading bytes from memory
+Frictionless supports reading bytes loaded into from memory.
 
 ## Reading Buffer Data
 
 You can read Buffer Data using `Package/Resource` or `Table` API, for example:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(b'id,name\n1,english\n2,german', format='csv')
 print(resource.read_rows())
 ```
-
-    [Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', 'german')])]
-
+```
+[Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', 'german')])]
+```
 
 ## Writing Buffer Data
 
 The same is actual for writing Buffer Data:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(data=[['id', 'name'], [1, 'english'], [2, 'german']])
 resource.write(scheme='buffer', format='csv')
 ```
-
-
-
-
-    b'id,name\r\n1,english\r\n2,german\r\n'
-
-
+```
+b'id,name\r\n1,english\r\n2,german\r\n'
+```
 
 ## Configuring Buffer Data
 
diff --git a/docs/tutorials/schemes/local-tutorial.md b/docs/tutorials/schemes/local-tutorial.md
index f9f9cea7..9e2bc5a4 100644
--- a/docs/tutorials/schemes/local-tutorial.md
+++ b/docs/tutorials/schemes/local-tutorial.md
@@ -9,34 +9,27 @@ You can read and write files locally with Frictionless. It's basic functionality
 
 You can read using `Package/Resource` or `Table` API, for example:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(path='data/table.csv')
 print(resource.read_rows())
 ```
-
-    [Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
-
+```
+[Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
+```
 
 ## Writing Local Data
 
 The same is actual for writing:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(data=[['id', 'name'], [1, 'english'], [2, 'german']])
 resource.write('tmp/table.csv')
 ```
 
-```bash
-!cat tmp/table.csv
-```
-
-
 ## Configuring Local Data
 
 There are no options available in `LocalControl`.
diff --git a/docs/tutorials/schemes/multipart-tutorial.md b/docs/tutorials/schemes/multipart-tutorial.md
index 845d0f04..932e517b 100644
--- a/docs/tutorials/schemes/multipart-tutorial.md
+++ b/docs/tutorials/schemes/multipart-tutorial.md
@@ -3,7 +3,7 @@ title: Multipart Tutorial
 sidebar_label: Multipart
 ---
 
-> This functionality requires an experimental `mulripart` plugin. [Read More](../../references/plugins-reference.md)
+> This functionality requires an experimental `multipart` plugin. [Read More](../../references/plugins-reference.md)
 
 You can read and write files split into chunks with Frictionless.
 
@@ -11,34 +11,32 @@ You can read and write files split into chunks with Frictionless.
 
 You can read using `Package/Resource` or `Table` API, for example:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(path=['data/chunk1.csv', 'data/chunk2.csv'])
 print(resource.read_rows())
 ```
-
-    [Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
-
+```
+[Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
+```
 
 ## Writing Multipart Data
 
 The actual for writing:
 
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(path='data/table.json')
 resource.write('tmp/table{number}.json', scheme="multipart", control={"chunkSize": 1000000})
 ```
 
-
 ## Configuring Local Data
 
 There is a control to configure how Frictionless read files using this scheme. For example:
 
-```python
+```python title="Python"
 from frictionless import Resource
 from frictionless.plugins.multipart import MultipartControl
 
@@ -47,6 +45,5 @@ resource = Resource(data=[['id', 'name'], [1, 'english'], [2, 'german']])
 resource.write('tmp/table{number}.json', scheme="multipart", control=control)
 ```
 
-
 References:
 - [Multipart Control](../../references/schemes-reference.md#multipart)
diff --git a/docs/tutorials/schemes/remote-tutorial.md b/docs/tutorials/schemes/remote-tutorial.md
index 761e616b..fcaf178a 100644
--- a/docs/tutorials/schemes/remote-tutorial.md
+++ b/docs/tutorials/schemes/remote-tutorial.md
@@ -7,37 +7,34 @@ You can read files remotely with Frictionless. It's basic functionality.
 
 ## Reading Remote Data
 
-You can read using `Package/Resource` or `Table` API, for example:
+You can read using `Package/Resource`, for example:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(path='https://raw.githubusercontent.com/frictionlessdata/frictionless-py/master/data/table.csv')
 print(resource.read_rows())
 ```
-
-    [Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
-
+```
+[Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
+```
 
 ## Writing Remote Data
 
 The save is actual for writing:
 
-```py
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(path='data/table.csv')
 resource.write('https://example.com/data/table.csv') # will POST the file to the server
 ```
 
-
 ## Configuring Remote Data
 
 There is a control to configure remote data, for example:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 from frictionless.plugins.remote import RemoteControl
 
@@ -45,9 +42,9 @@ control = RemoteControl(http_timeout=10)
 resource = Resource(path='https://raw.githubusercontent.com/frictionlessdata/frictionless-py/master/data/table.csv', control=control)
 print(resource.read_rows())
 ```
-
-    [Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
-
+```
+[Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
+```
 
 References:
 - [Remote Control](../../references/schemes-reference.md#remote)
diff --git a/docs/tutorials/schemes/s3-tutorial.md b/docs/tutorials/schemes/s3-tutorial.md
index e9525f1b..ff52f032 100644
--- a/docs/tutorials/schemes/s3-tutorial.md
+++ b/docs/tutorials/schemes/s3-tutorial.md
@@ -7,42 +7,39 @@ sidebar_label: S3
 
 Frictionless supports reading data from S3 cloud source. You can read file in any format that is available in your bucket.
 
-```bash
-! pip install frictionless[s3]
+```bash title="CLI"
+pip install frictionless[s3]
 ```
 
-
 ## Reading from S3
 
 You can read from this source using `Package/Resource` or `Table` API, for example:
 
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(path='s3://bucket/table.csv')
 print(resource.read_rows())
 ```
 
-
 For reading from a private bucket you need to setup AWS creadentials as it's described in [Boto3 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#environment-variables).
 
 ## Writing to S3
 
 The same is actual for writing:
 
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(path='data/table.csv')
 resource.write('s3://bucket/table.csv')
 ```
 
-
 ## Configuring S3
 
 There is a control to configure how Frictionless read files in this storage. For example:
 
-```python
+```python title="Python"
 from frictionless import Resource
 from frictionless.plugins.s3 import S3Control
 
@@ -50,6 +47,5 @@ resource = Resource(data=[['id', 'name'], [1, 'english'], [2, 'german']])
 resource.write('table.new.csv', control=controls.S3Control(endpoint_url='<url>'))
 ```
 
-
 References:
 - [S3 Control](../../references/schemes-reference.md#s3)
diff --git a/docs/tutorials/schemes/stream-tutorial.md b/docs/tutorials/schemes/stream-tutorial.md
index b28a6adf..c3867e78 100644
--- a/docs/tutorials/schemes/stream-tutorial.md
+++ b/docs/tutorials/schemes/stream-tutorial.md
@@ -7,38 +7,34 @@ Frictionless supports loading Stream data.
 
 ## Reading Stream Data
 
-You can read Stream using `Package/Resource` or `Table` API, for example:
+> It's recommended to open files in byte-mode. If it's opened in text-mode, Frictionless will try re-open it in byte-mode.
 
+You can read Stream using `Package/Resource` or `Table` API, for example:
 
-```python
+```python title="Python"
 from frictionless import Resource
 
 with open('data/table.csv', 'rb') as file:
   resource = Resource(file, format='csv')
   print(resource.read_rows())
 ```
-
-    [Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
-
+```
+[Row([('id', 1), ('name', 'english')]), Row([('id', 2), ('name', '中国人')])]
+```
 
 ## Writing Stream Data
 
 The same is actual for writing CSV:
 
-
-```python
+```python title="Python"
 from frictionless import Resource
 
 resource = Resource(data=[['id', 'name'], [1, 'english'], [2, 'german']])
 resource.write(scheme='stream', format='csv')
 ```
-
-
-
-
-    <_io.BufferedReader name='/tmp/tmplh6mlh54'>
-
-
+```
+<_io.BufferedReader name='/tmp/tmplh6mlh54'>
+```
 
 ## Configuring Stream Data
 
diff --git a/frictionless/plugins/stream.py b/frictionless/plugins/stream.py
index 155b4d1f..e016d4d0 100644
--- a/frictionless/plugins/stream.py
+++ b/frictionless/plugins/stream.py
@@ -1,3 +1,4 @@
+import os
 from ..plugin import Plugin
 from ..loader import Loader
 from ..control import Control
@@ -76,13 +77,40 @@ class StreamLoader(Loader):
     # Read
 
     def read_byte_stream_create(self):
-        data = self.resource.data
-        if hasattr(data, "encoding"):
-            error = errors.SchemeError(note="only byte streams are supported")
-            raise FrictionlessException(error)
-        return data
+        byte_stream = self.resource.data
+        if not os.path.isfile(byte_stream.name):
+            note = f"only local streams are supported: {byte_stream}"
+            raise FrictionlessException(errors.SchemeError(note=note))
+        if hasattr(byte_stream, "encoding"):
+            try:
+                byte_stream = open(byte_stream.name, "rb")
+            except Exception:
+                note = f"cannot open a stream in the byte mode: {byte_stream}"
+                raise FrictionlessException(errors.SchemeError(note=note))
+        byte_stream = ReusableByteStream(byte_stream)
+        return byte_stream
 
     # Write
 
     def write_byte_stream_save(self, byte_stream):
         self.resource.data = byte_stream
+
+
+# Internal
+
+
+class ReusableByteStream:
+    def __init__(self, byte_stream):
+        self.__byte_stream = byte_stream
+
+    def __getattr__(self, name):
+        return getattr(self.__byte_stream, name)
+
+    def read(self, size=-1):
+        if self.__byte_stream.closed:
+            try:
+                self.__byte_stream = open(self.__byte_stream.name, "rb")
+            except Exception:
+                note = "cannot re-open a byte stream: {self.__byte_stream}"
+                raise FrictionlessException(errors.SchemeError(note=note))
+        return self.__byte_stream.read(size)


Test patch:
diff --git a/tests/plugins/test_stream.py b/tests/plugins/test_stream.py
index 8b75a2d0..ee36a9aa 100644
--- a/tests/plugins/test_stream.py
+++ b/tests/plugins/test_stream.py
@@ -1,4 +1,4 @@
-from frictionless import Resource
+from frictionless import Resource, validate
 
 
 # Loader
@@ -14,6 +14,16 @@ def test_stream_loader():
             ]
 
 
+def test_stream_loader_text_stream():
+    with open("data/table.csv") as file:
+        with Resource(file, format="csv") as resource:
+            assert resource.header == ["id", "name"]
+            assert resource.read_rows() == [
+                {"id": 1, "name": "english"},
+                {"id": 2, "name": "中国人"},
+            ]
+
+
 def test_stream_loader_without_open():
     with open("data/table.csv", mode="rb") as file:
         resource = Resource(file, format="csv")
@@ -31,3 +41,15 @@ def test_stream_loader_write():
             {"id": 1, "name": "english"},
             {"id": 2, "name": "中国人"},
         ]
+
+
+def test_stream_loader_validate_issue_740():
+    with open("data/table.csv", mode="rb") as file:
+        report = validate(file, format="csv")
+        assert report.valid
+
+
+def test_stream_loader_validate_text_stream_issue_740():
+    with open("data/table.csv") as file:
+        report = validate(file, format="csv")
+        assert report.valid
diff --git a/tests/test_resource.py b/tests/test_resource.py
index 3d07b12e..367c84a3 100644
--- a/tests/test_resource.py
+++ b/tests/test_resource.py
@@ -1,4 +1,3 @@
-import io
 import os
 import sys
 import json
@@ -521,8 +520,8 @@ def test_resource_encoding_explicit_latin1():
 
 def test_resource_encoding_utf_16():
     # Bytes encoded as UTF-16 with BOM in platform order is detected
-    bio = io.BytesIO(u"en,English\nja,日本語".encode("utf-16"))
-    with Resource(bio, format="csv", layout={"header": False}) as resource:
+    source = "en,English\nja,日本語".encode("utf-16")
+    with Resource(source, format="csv", layout={"header": False}) as resource:
         assert resource.encoding == "utf-16"
         assert resource.read_rows() == [
             {"field1": "en", "field2": "English"},
diff --git a/tests/validate/test_resource.py b/tests/validate/test_resource.py
index c93900be..5ec7fcd5 100644
--- a/tests/validate/test_resource.py
+++ b/tests/validate/test_resource.py
@@ -1141,9 +1141,8 @@ def test_validate_missing_local_file_raises_scheme_error_issue_315():
     ]
 
 
+# Text streams are not supported
 def test_validate_inline_not_a_binary_issue_349():
     with open("data/table.csv") as source:
         report = validate(source)
-        assert report.flatten(["code", "note"]) == [
-            ["scheme-error", "only byte streams are supported"],
-        ]
+        assert report.valid