Repository: Textualize/rich
Pull request #2495 (2022-08-27 15:30:33)

Fix missing `mode` property on file wrapper
Add the missing `mode` property to the file wrapper used by `rich.progress.wrap_file`.

This fixes an incompatibility with the [requests](https://github.com/psf/requests) library.

## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [ ] Tests
- [ ] Other

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

Without this property, uploads using `requests` fail to set the `Content-Length` header, and fall back to chunked encoding,
which many hosts (e.g. S3) don't support.

`requests` uses the `mode` property in its
[`super_len`](https://github.com/psf/requests/blob/177dd90f18a8f4dc79a7d2049f0a3f4fcc5932a0/requests/utils.py#L151)
function, which determines the length of the stream to upload.

### Example Code (Fragment)
```python
def upload_stream(session: requests.Session, source: BinaryIO, remote_url: str, size: int, label: str):
    """Upload a data stream from `source `to `remote_url``."""
    with wrap_file(source, size, description=label) as f:
        response = session.put(
            url=remote_url,
            data=f,
        )
   response. raise_for_status()


def upload_file(session: requests.Session, source: pathlib.Path, remote_url: str):
    """Upload a file from `source `to `remote_url``."""
    with source.open(mode="rb") as f:
        upload_stream(
            session,
            f,
            remote_url=remote_url,
            size=source.stat().st_size,
            label=source.name,
        )
```

### Before fix
**Request headers**
```
User-Agent: python-requests/2.28.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Transfer-Encoding: chunked
```
**Response**
```
501 Not Implemented
x-amz-request-id: ...
x-amz-id-2: ...
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Sat, 27 Aug 2022 14:57:45 GMT
Server: AmazonS3
Connection: close
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NotImplemented</Code><Message>A header you provided implies functionality that is not implemented</Message><Header>Transfer-Encoding</Header><RequestId>...</RequestId><HostId>...=</HostId></Error>
```

### After fix
**Request headers**
```
User-Agent: python-requests/2.28.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 9701
```
**Response**
```
200 OK
x-amz-id-2: ...
x-amz-request-id: ...
Date: Sat, 27 Aug 2022 14:43:32 GMT
ETag: "..."
Server: AmazonS3
Content-Length: 0
```



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b723ded..2d14fff1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 
 - Fix NO_COLOR support on legacy Windows https://github.com/Textualize/rich/pull/2458
+- Fix missing `mode` property on file wrapper breaking uploads via `requests` https://github.com/Textualize/rich/pull/2495
 
 ## [12.5.2] - 2022-07-18
 
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 61125989..7890ec10 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -28,6 +28,7 @@ The following people have contributed to the development of Rich:
 - [Paul McGuire](https://github.com/ptmcg)
 - [Antony Milne](https://github.com/AntonyMilneQB)
 - [Michael Milton](https://github.com/multimeric)
+- [Martina Oefelein](https://github.com/oefe)
 - [Nathan Page](https://github.com/nathanrpage97)
 - [Avi Perl](https://github.com/avi-perl)
 - [Laurent Peuch](https://github.com/psycojoker)
diff --git a/rich/progress.py b/rich/progress.py
index 679731a1..9873e1a5 100644
--- a/rich/progress.py
+++ b/rich/progress.py
@@ -216,6 +216,10 @@ class _Reader(RawIOBase, BinaryIO):
     def isatty(self) -> bool:
         return self.handle.isatty()
 
+    @property
+    def mode(self) -> str:
+        return self.handle.mode
+
     @property
     def name(self) -> str:
         return self.handle.name


Test patch:
diff --git a/tests/test_progress.py b/tests/test_progress.py
index 792ee636..a3a28766 100644
--- a/tests/test_progress.py
+++ b/tests/test_progress.py
@@ -614,6 +614,7 @@ def test_wrap_file() -> None:
         with open(filename, "rb") as file:
             with rich.progress.wrap_file(file, total=total) as f:
                 assert f.read() == b"Hello, World!"
+                assert f.mode == "rb"
                 assert f.name == filename
             assert f.closed
             assert not f.handle.closed