Repository: onicagroup/runway

Issue:
[TODO] add `_bool_` to `runway.core.providers.aws.s3._bucket.Bucket`
- `False` if `.forbidden` or `not .exists`
- `True` if `.exists`
- assess if `.exists` should be `False` if `forbidden`


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/onicagroup/runway/pull/1133?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup) Report
> Merging [#1133](https://codecov.io/gh/onicagroup/runway/pull/1133?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup) (f17c568) into [master](https://codecov.io/gh/onicagroup/runway/commit/35e3fe7c4479d585c9d9a41d8848a22ef1840d68?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup) (35e3fe7) will **decrease** coverage by `0.00%`.
> The diff coverage is `83.33%`.

[![Impacted file tree graph](https://codecov.io/gh/onicagroup/runway/pull/1133/graphs/tree.svg?width=650&height=150&src=pr&token=Ku28I0RY80&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup)](https://codecov.io/gh/onicagroup/runway/pull/1133?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup)

```diff
@@            Coverage Diff             @@
##           master    #1133      +/-   ##
==========================================
- Coverage   89.48%   89.47%   -0.01%     
==========================================
  Files         247      247              
  Lines       15295    15297       +2     
==========================================
+ Hits        13686    13687       +1     
- Misses       1609     1610       +1     
```


| [Impacted Files](https://codecov.io/gh/onicagroup/runway/pull/1133?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup) | Coverage Δ | |
|---|---|---|
| [runway/core/providers/aws/s3/\_bucket.py](https://codecov.io/gh/onicagroup/runway/pull/1133/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup#diff-cnVud2F5L2NvcmUvcHJvdmlkZXJzL2F3cy9zMy9fYnVja2V0LnB5) | `98.59% <83.33%> (-1.41%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/onicagroup/runway/pull/1133?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/onicagroup/runway/pull/1133?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup). Last update [35e3fe7...f17c568](https://codecov.io/gh/onicagroup/runway/pull/1133?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=onicagroup).


Patch:
diff --git a/runway/core/providers/aws/s3/_bucket.py b/runway/core/providers/aws/s3/_bucket.py
index fe0faffa..d0f508b5 100644
--- a/runway/core/providers/aws/s3/_bucket.py
+++ b/runway/core/providers/aws/s3/_bucket.py
@@ -53,14 +53,10 @@ class Bucket(DelCachedPropMixin):
 
     @property
     def exists(self) -> bool:
-        """Check whether the bucket exists.
+        """Check whether the bucket exists and is accessible."""
+        return not self.forbidden and not self.not_found
 
-        Opposite of not_found.
-
-        """
-        return not self.not_found
-
-    @cached_property
+    @property
     def forbidden(self) -> bool:
         """Check whether access to the bucket is forbidden."""
         return self.head.metadata.forbidden
@@ -85,7 +81,7 @@ class Bucket(DelCachedPropMixin):
             )
             return BaseResponse.parse_obj(err.response)
 
-    @cached_property
+    @property
     def not_found(self) -> bool:
         """Check whether the bucket exists."""
         return self.head.metadata.not_found
@@ -120,7 +116,7 @@ class Bucket(DelCachedPropMixin):
                 {"LocationConstraint": self.client.meta.region_name}
             )
         LOGGER.debug("creating bucket: %s", json.dumps(kwargs))
-        self._del_cached_property("forbidden", "not_found", "head")
+        self._del_cached_property("head")
         return self.client.create_bucket(**kwargs)
 
     def enable_versioning(self) -> None:
@@ -239,3 +235,7 @@ class Bucket(DelCachedPropMixin):
             session=self.session,
             src=self.format_bucket_path_uri(prefix=prefix),
         ).run()
+
+    def __bool__(self) -> bool:
+        """Implement evaluation of instances as a bool."""
+        return self.exists


Test patch:
diff --git a/tests/unit/core/providers/aws/s3/test_bucket.py b/tests/unit/core/providers/aws/s3/test_bucket.py
index 7df842ed..2a93dc29 100644
--- a/tests/unit/core/providers/aws/s3/test_bucket.py
+++ b/tests/unit/core/providers/aws/s3/test_bucket.py
@@ -7,6 +7,7 @@ import logging
 from http import HTTPStatus
 from typing import TYPE_CHECKING
 
+import pytest
 from mock import MagicMock
 
 from runway.core.providers.aws import BaseResponse
@@ -24,6 +25,31 @@ MODULE = "runway.core.providers.aws.s3._bucket"
 class TestBucket:
     """Test runway.core.providers.aws.s3._bucket.Bucket."""
 
+    @pytest.mark.parametrize(
+        "forbidden, not_found, expected",
+        [
+            (False, False, True),
+            (False, True, False),
+            (True, False, False),
+        ],
+    )
+    def test___bool__(
+        self,
+        expected: bool,
+        forbidden: bool,
+        mocker: MockerFixture,
+        not_found: bool,
+        runway_context: MockRunwayContext,
+    ) -> None:
+        """Test __bool__."""
+        response = BaseResponse()
+        if forbidden:
+            response.metadata.http_status_code = HTTPStatus.FORBIDDEN
+        elif not_found:
+            response.metadata.http_status_code = HTTPStatus.NOT_FOUND
+        mocker.patch.object(Bucket, "head", response)
+        assert Bucket(runway_context, "test-bucket").exists is expected
+
     def test_client(self) -> None:
         """Test client."""
         mock_ctx = MagicMock()
@@ -170,37 +196,46 @@ class TestBucket:
             'did not modify versioning policy for bucket "test-bucket"; already enabled'
         ) in caplog.messages
 
+    @pytest.mark.parametrize(
+        "forbidden, not_found, expected",
+        [
+            (False, False, True),
+            (False, True, False),
+            (True, False, False),
+        ],
+    )
     def test_exists(
-        self, mocker: MockerFixture, runway_context: MockRunwayContext
+        self,
+        expected: bool,
+        forbidden: bool,
+        mocker: MockerFixture,
+        not_found: bool,
+        runway_context: MockRunwayContext,
     ) -> None:
         """Test not_found."""
-        mock_head = mocker.patch.object(Bucket, "head", spec=BaseResponse())
-        bucket = Bucket(runway_context, "test-bucket")
-
-        mock_head.metadata.not_found = True
-        assert not bucket.exists  # initial value
-
-        mock_head.metadata.not_found = False
-        assert not bucket.exists  # cached value
-        del bucket.not_found
-
-        assert bucket.exists  # updated value
-
+        response = BaseResponse()
+        if forbidden:
+            response.metadata.http_status_code = HTTPStatus.FORBIDDEN
+        elif not_found:
+            response.metadata.http_status_code = HTTPStatus.NOT_FOUND
+        mocker.patch.object(Bucket, "head", response)
+        assert Bucket(runway_context, "test-bucket").exists is expected
+
+    @pytest.mark.parametrize("forbidden, expected", [(True, True), (False, False)])
     def test_forbidden(
-        self, mocker: MockerFixture, runway_context: MockRunwayContext
+        self,
+        expected: bool,
+        forbidden: bool,
+        mocker: MockerFixture,
+        runway_context: MockRunwayContext,
     ) -> None:
         """Test forbidden."""
-        mock_head = mocker.patch.object(Bucket, "head", spec=BaseResponse())
-        bucket = Bucket(runway_context, "test-bucket")
-
-        mock_head.metadata.forbidden = True
-        assert bucket.forbidden  # initial value
-
-        mock_head.metadata.forbidden = False
-        assert bucket.forbidden  # cached value
-        del bucket.forbidden
-
-        assert not bucket.forbidden  # updated value
+        response = BaseResponse()
+        response.metadata.http_status_code = (
+            HTTPStatus.FORBIDDEN if forbidden else HTTPStatus.OK
+        )
+        mocker.patch.object(Bucket, "head", response)
+        assert Bucket(runway_context, "test-bucket").forbidden is expected
 
     def test_format_bucket_path_uri(self) -> None:
         """Test format_bucket_path_uri."""
@@ -266,21 +301,21 @@ class TestBucket:
         stubber.assert_no_pending_responses()
         assert "received an error from AWS S3" in "\n".join(caplog.messages)
 
+    @pytest.mark.parametrize("not_found, expected", [(True, True), (False, False)])
     def test_not_found(
-        self, mocker: MockerFixture, runway_context: MockRunwayContext
+        self,
+        expected: bool,
+        mocker: MockerFixture,
+        not_found: bool,
+        runway_context: MockRunwayContext,
     ) -> None:
         """Test not_found."""
-        mock_head = mocker.patch.object(Bucket, "head", spec=BaseResponse())
-        bucket = Bucket(runway_context, "test-bucket")
-
-        mock_head.metadata.not_found = True
-        assert bucket.not_found  # initial value
-
-        mock_head.metadata.not_found = False
-        assert bucket.not_found  # cached value
-        del bucket.not_found
-
-        assert not bucket.not_found  # updated value
+        response = BaseResponse()
+        response.metadata.http_status_code = (
+            HTTPStatus.NOT_FOUND if not_found else HTTPStatus.OK
+        )
+        mocker.patch.object(Bucket, "head", response)
+        assert Bucket(runway_context, "test-bucket").not_found is expected
 
     def test_sync_from_local(
         self, mocker: MockerFixture, runway_context: MockRunwayContext