Repository: aio-libs/aioftp

Issue:
Windows_list parser fails on date_time format
Based on this [issue](https://github.com/aio-libs/aioftp/issues/82):

Hello everyone, seems we've got some problem with `parse_list_line_windows` function.

```python3
with setlocale("C"):
    strptime = datetime.datetime.strptime
    date_time = strptime(date_time_str, "%m/%d/%Y %I:%M %p")
```
In this case date cannot be parsed properly, so we've got an empty result of `client.list()` function.

We've made some changes to the date format to make it works:
```python3
with setlocale("C"):
    strptime = datetime.datetime.strptime
    date_time = strptime(date_time_str, "%m-%d-%y %I:%M%p")
```

Actually we couldn't understand the role of `setlocale` context, at this time we already have a date as string and setlocale seems doing nothing to this. Probably we should add optional argument for date-time format.

Comments:

Patch:
diff --git a/aioftp/client.py b/aioftp/client.py
index 007eba7..61c7b85 100644
--- a/aioftp/client.py
+++ b/aioftp/client.py
@@ -106,7 +106,8 @@ class BaseClient:
     def __init__(self, *, socket_timeout=None,
                  read_speed_limit=None, write_speed_limit=None,
                  path_timeout=None, path_io_factory=pathio.PathIO,
-                 encoding="utf-8", ssl=None, **siosocks_asyncio_kwargs):
+                 encoding="utf-8", ssl=None, parse_list_line_custom=None,
+                 **siosocks_asyncio_kwargs):
         self.socket_timeout = socket_timeout
         self.throttle = StreamThrottle.from_limits(
             read_speed_limit,
@@ -117,6 +118,7 @@ class BaseClient:
         self.encoding = encoding
         self.stream = None
         self.ssl = ssl
+        self.parse_list_line_custom = parse_list_line_custom
         self._open_connection = partial(open_connection, ssl=self.ssl,
                                         **siosocks_asyncio_kwargs)
 
@@ -480,8 +482,14 @@ class BaseClient:
         :rtype: (:py:class:`pathlib.PurePosixPath`, :py:class:`dict`)
         """
         ex = []
-        parsers = (self.parse_list_line_unix, self.parse_list_line_windows)
+        parsers = (
+            self.parse_list_line_custom,
+            self.parse_list_line_unix,
+            self.parse_list_line_windows,
+        )
         for parser in parsers:
+            if parser is None:
+                continue
             try:
                 return parser(b)
             except (ValueError, KeyError, IndexError) as e:
@@ -543,6 +551,11 @@ class Client(BaseClient):
         Please look :py:meth:`asyncio.loop.create_connection` docs.
     :type ssl: :py:class:`bool` or :py:class:`ssl.SSLContext`
 
+    :param parse_list_line_custom: callable, which receive exactly one
+        argument: line of type bytes. Should return tuple of Path object and
+        dictionary with fields "modify", "type", "type", "size". For more
+        information see sources.
+    :type parse_list_line_custom: callable
     :param **siosocks_asyncio_kwargs: siosocks key-word only arguments
     """
     async def connect(self, host, port=DEFAULT_PORT):
diff --git a/history.rst b/history.rst
index f64c8c4..9128de3 100644
--- a/history.rst
+++ b/history.rst
@@ -2,6 +2,9 @@
 -------------------
 
 - client: add socks proxy support via `siosocks <https://github.com/pohmelie/siosocks>`_ (#94)
+- client: add custom `list` parser (#95)
+
+Thanks to `purpleskyfall https://github.com/purpleskyfall`_, `VyachAp <https://github.com/VyachAp>`_
 
 0.13.0 (24-03-2019)
 -------------------


Test patch:
diff --git a/tests/test_list_fallback.py b/tests/test_list_fallback.py
index f48a89a..6aeb03a 100644
--- a/tests/test_list_fallback.py
+++ b/tests/test_list_fallback.py
@@ -90,3 +90,26 @@ def test_client_list_windows():
         assert entities[p]["type"] == "file"
     with pytest.raises(ValueError):
         parse(b" 10/3/2018   2:58 PM    34,35xxx38,368  win10.img")
+
+
+@pytest.mark.asyncio
+async def test_client_list_override_with_custom(pair_factory, Client):
+    meta = {"type": "file", "works": True}
+
+    def parser(b):
+        import pickle
+        return pickle.loads(bytes.fromhex(b.decode().rstrip("\r\n")))
+
+    async def builder(_, path):
+        import pickle
+        return pickle.dumps((path, meta)).hex()
+
+    async with pair_factory(Client(parse_list_line_custom=parser)) as pair:
+        pair.server.mlst = not_implemented
+        pair.server.mlsd = not_implemented
+        pair.server.build_list_string = builder
+        await pair.client.make_directory("bar")
+        (path, stat), *_ = files = await pair.client.list()
+        assert len(files) == 1
+        assert path == pathlib.PurePosixPath("bar")
+        assert stat == meta