Skip to content

Commit ed1e6ce

Browse files
authored
Merge pull request #369 from praw-dev/sort-code
Add and apply code sorting pre-commit hook
2 parents 62d5ec5 + 7a24919 commit ed1e6ce

26 files changed

Lines changed: 953 additions & 910 deletions

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ repos:
55
repo: https://github.com/LilSpazJoekp/docstrfmt
66
rev: 06455715c8a81e176748e036c1c945f3e80d5b8d # frozen: v2.1.0
77

8+
- hooks:
9+
- id: codesorter
10+
require_serial: true
11+
repo: https://github.com/praw-dev/CodeSorter
12+
rev: a8b600db67edfc9f71a13e97f67c10d72f87fec7 # frozen: unreleased
13+
814
- hooks:
915
- id: auto-walrus
1016
repo: https://github.com/MarcoGorelli/auto-walrus

asyncpraw/config.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,39 +57,10 @@ def _config_boolean(*, item: bool | str | _NotSet) -> bool:
5757
return False
5858
return item.lower() in {"1", "yes", "true", "on"}
5959

60-
@classmethod
61-
def _load_config(cls, *, config_interpolation: str | None = None) -> None:
62-
"""Attempt to load settings from various praw.ini files."""
63-
if config_interpolation is not None:
64-
interpolator_class = cls.INTERPOLATION_LEVEL[config_interpolation]()
65-
else:
66-
interpolator_class = None
67-
68-
config = configparser.ConfigParser(interpolation=interpolator_class)
69-
assert __package__ is not None
70-
with files(__package__).joinpath("praw.ini").open("r") as hdl:
71-
config.read_file(hdl)
72-
73-
if "APPDATA" in os.environ: # Windows
74-
os_config_path = Path(os.environ["APPDATA"])
75-
elif "XDG_CONFIG_HOME" in os.environ: # Modern Linux
76-
os_config_path = Path(os.environ["XDG_CONFIG_HOME"])
77-
elif "HOME" in os.environ: # Legacy Linux
78-
os_config_path = Path(os.environ["HOME"]) / ".config"
79-
else:
80-
os_config_path = None
81-
82-
locations = ["praw.ini"]
83-
84-
if os_config_path is not None:
85-
locations.insert(0, str(os_config_path / "praw.ini"))
86-
87-
cls._warn_on_endpoint_override(interpolator_class)
88-
config.read(locations)
89-
cls.CONFIG = config
90-
9160
@staticmethod
92-
def _warn_on_endpoint_override(interpolator_class: configparser.Interpolation | None) -> None:
61+
def _warn_on_endpoint_override(
62+
interpolator_class: configparser.Interpolation | None,
63+
) -> None:
9364
"""Warn if a ``praw.ini`` in the current directory overrides OAuth endpoints.
9465
9566
``praw.ini`` is loaded from the current working directory, so a file planted
@@ -124,6 +95,37 @@ def _warn_on_endpoint_override(interpolator_class: configparser.Interpolation |
12495
stacklevel=4,
12596
)
12697

98+
@classmethod
99+
def _load_config(cls, *, config_interpolation: str | None = None) -> None:
100+
"""Attempt to load settings from various praw.ini files."""
101+
if config_interpolation is not None:
102+
interpolator_class = cls.INTERPOLATION_LEVEL[config_interpolation]()
103+
else:
104+
interpolator_class = None
105+
106+
config = configparser.ConfigParser(interpolation=interpolator_class)
107+
assert __package__ is not None
108+
with files(__package__).joinpath("praw.ini").open("r") as hdl:
109+
config.read_file(hdl)
110+
111+
if "APPDATA" in os.environ: # Windows
112+
os_config_path = Path(os.environ["APPDATA"])
113+
elif "XDG_CONFIG_HOME" in os.environ: # Modern Linux
114+
os_config_path = Path(os.environ["XDG_CONFIG_HOME"])
115+
elif "HOME" in os.environ: # Legacy Linux
116+
os_config_path = Path(os.environ["HOME"]) / ".config"
117+
else:
118+
os_config_path = None
119+
120+
locations = ["praw.ini"]
121+
122+
if os_config_path is not None:
123+
locations.insert(0, str(os_config_path / "praw.ini"))
124+
125+
cls._warn_on_endpoint_override(interpolator_class)
126+
config.read(locations)
127+
cls.CONFIG = config
128+
127129
@property
128130
def short_url(self) -> str:
129131
"""Return the short url.

asyncpraw/models/base.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,6 @@
1010
import asyncpraw
1111

1212

13-
class DynamicAttributes:
14-
"""Mixin for objects whose attributes are populated from Reddit response data.
15-
16-
Reddit adds and removes fields without notice, so Async PRAW sets these attributes
17-
dynamically rather than declaring them. Defining ``__getattr__`` (typed to return
18-
``Any``) tells type checkers that attribute access on such objects is permitted,
19-
which is required for downstream projects to type check against Async PRAW's
20-
``py.typed`` marker. :class:`.RedditBase` provides equivalent behavior (with lazy
21-
fetching) for the objects it backs; this mixin covers the :class:`.AsyncPRAWBase`
22-
data classes that do not inherit it.
23-
24-
It does not change runtime behavior: accessing a genuinely missing attribute still
25-
raises :py:class:`AttributeError`.
26-
27-
"""
28-
29-
def __getattr__(self, attribute: str) -> Any:
30-
"""Raise :py:class:`AttributeError` for a missing dynamic attribute."""
31-
msg = f"{self.__class__.__name__!r} object has no attribute {attribute!r}"
32-
raise AttributeError(msg)
33-
34-
3513
class AsyncPRAWBase:
3614
"""Superclass for all models in Async PRAW."""
3715

@@ -81,3 +59,25 @@ def __init__(self, reddit: asyncpraw.Reddit, _data: dict[str, Any] | None) -> No
8159
if _data:
8260
for attribute, value in _data.items():
8361
setattr(self, attribute, value)
62+
63+
64+
class DynamicAttributes:
65+
"""Mixin for objects whose attributes are populated from Reddit response data.
66+
67+
Reddit adds and removes fields without notice, so Async PRAW sets these attributes
68+
dynamically rather than declaring them. Defining ``__getattr__`` (typed to return
69+
``Any``) tells type checkers that attribute access on such objects is permitted,
70+
which is required for downstream projects to type check against Async PRAW's
71+
``py.typed`` marker. :class:`.RedditBase` provides equivalent behavior (with lazy
72+
fetching) for the objects it backs; this mixin covers the :class:`.AsyncPRAWBase`
73+
data classes that do not inherit it.
74+
75+
It does not change runtime behavior: accessing a genuinely missing attribute still
76+
raises :py:class:`AttributeError`.
77+
78+
"""
79+
80+
def __getattr__(self, attribute: str) -> Any:
81+
"""Raise :py:class:`AttributeError` for a missing dynamic attribute."""
82+
msg = f"{self.__class__.__name__!r} object has no attribute {attribute!r}"
83+
raise AttributeError(msg)

asyncpraw/models/list/base.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ class BaseList(AsyncPRAWBase):
1717

1818
CHILD_ATTRIBUTE: ClassVar[str | None] = None
1919

20-
def _child_attribute(self) -> str:
21-
"""Return ``CHILD_ATTRIBUTE``, ensuring it has been set by a subclass."""
22-
if self.CHILD_ATTRIBUTE is None:
23-
msg = "BaseList must be extended."
24-
raise NotImplementedError(msg)
25-
return self.CHILD_ATTRIBUTE
26-
2720
def __contains__(self, item: Any) -> bool:
2821
"""Test if item exists in the list."""
2922
return item in getattr(self, self._child_attribute())
@@ -55,3 +48,10 @@ def __len__(self) -> int:
5548
def __str__(self) -> str:
5649
"""Return a string representation of the list."""
5750
return str(getattr(self, self._child_attribute()))
51+
52+
def _child_attribute(self) -> str:
53+
"""Return ``CHILD_ATTRIBUTE``, ensuring it has been set by a subclass."""
54+
if self.CHILD_ATTRIBUTE is None:
55+
msg = "BaseList must be extended."
56+
raise NotImplementedError(msg)
57+
return self.CHILD_ATTRIBUTE

asyncpraw/models/listing/generator.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,6 @@
1414
from asyncpraw.models.reddit.base import RedditBase
1515

1616

17-
class ListingGeneratorKwargs(TypedDict, total=False):
18-
"""The keyword arguments accepted by methods that return a :class:`.ListingGenerator`.
19-
20-
See :meth:`.ListingGenerator.__init__` for the meaning of each value.
21-
22-
"""
23-
24-
limit: int | None
25-
params: dict[str, str | int] | None
26-
27-
2817
class ListingGenerator(AsyncPRAWBase, AsyncIterator):
2918
"""Instances of this class generate :class:`.RedditBase` instances.
3019
@@ -118,3 +107,14 @@ async def _next_batch(self) -> None:
118107
self.params[self._listing.AFTER_PARAM] = self._listing.after
119108
else:
120109
self._exhausted = True
110+
111+
112+
class ListingGeneratorKwargs(TypedDict, total=False):
113+
"""The keyword arguments accepted by methods that return a :class:`.ListingGenerator`.
114+
115+
See :meth:`.ListingGenerator.__init__` for the meaning of each value.
116+
117+
"""
118+
119+
limit: int | None
120+
params: dict[str, str | int] | None

asyncpraw/models/media.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ class Media:
4242
LEASE_API_PATH: ClassVar[str]
4343
LEASE_RESPONSE_KEY: ClassVar[str] = "s3UploadLease"
4444

45+
@staticmethod
46+
async def _raise_upload_error(response: ClientResponse, /) -> None:
47+
raise ServerError(response)
48+
49+
@property
50+
def _mime_type(self) -> str:
51+
if self._mime_type_value is None:
52+
mime_type, _ = guess_file_type(self.name)
53+
if mime_type is None:
54+
msg = f"Unable to determine the MIME type of {self.name!r}."
55+
raise ClientException(msg)
56+
self._mime_type_value = mime_type
57+
return self._mime_type_value
58+
4559
def __eq__(self, other: object) -> bool:
4660
"""Return whether the other instance equals the current."""
4761
return type(other) is type(self) and self.name == other.name and self._fp == other._fp
@@ -77,15 +91,8 @@ def __repr__(self) -> str:
7791
"""Return a string representation of the instance."""
7892
return f"<{self.__class__.__name__} name={self.name!r}>"
7993

80-
@property
81-
def _mime_type(self) -> str:
82-
if self._mime_type_value is None:
83-
mime_type, _ = guess_file_type(self.name)
84-
if mime_type is None:
85-
msg = f"Unable to determine the MIME type of {self.name!r}."
86-
raise ClientException(msg)
87-
self._mime_type_value = mime_type
88-
return self._mime_type_value
94+
def _build_lease_data(self, **additional_data: str) -> dict[str, str]:
95+
return {"filepath": self.name, "mimetype": self._mime_type, **additional_data}
8996

9097
async def _build_payload(self) -> BytesIO:
9198
"""Read the media content and wrap it in a named file-like object."""
@@ -98,9 +105,6 @@ async def _build_payload(self) -> BytesIO:
98105
payload.name = self.name
99106
return payload
100107

101-
def _build_lease_data(self, **additional_data: str) -> dict[str, str]:
102-
return {"filepath": self.name, "mimetype": self._mime_type, **additional_data}
103-
104108
async def _lease_and_post(
105109
self, lease_url: str, reddit: asyncpraw.Reddit, /, **additional_lease_data: str
106110
) -> tuple[dict[str, Any], dict[str, str], str]:
@@ -125,10 +129,6 @@ async def _post_to_s3(self, reddit: asyncpraw.Reddit, upload_data: dict[str, str
125129
if not response.ok:
126130
await self._raise_upload_error(response)
127131

128-
@staticmethod
129-
async def _raise_upload_error(response: ClientResponse, /) -> None:
130-
raise ServerError(response)
131-
132132
async def _upload(self, subreddit: models.Subreddit, /, **additional_lease_data: str) -> str:
133133
"""Upload the media to Reddit.
134134
@@ -194,7 +194,12 @@ async def _raise_upload_error(response: ClientResponse, /) -> None:
194194
await Media._raise_upload_error(response)
195195

196196
async def _upload( # pyright: ignore[reportIncompatibleMethodOverride] # post media is uploaded with a Reddit instance rather than a Subreddit
197-
self, reddit: asyncpraw.Reddit, /, *, expected_mime_prefix: str | None = None, upload_type: str = "link"
197+
self,
198+
reddit: asyncpraw.Reddit,
199+
/,
200+
*,
201+
expected_mime_prefix: str | None = None,
202+
upload_type: str = "link",
198203
) -> str:
199204
"""Upload the media to Reddit (undocumented endpoint).
200205

asyncpraw/models/reddit/announcement.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@ class Announcement(FullnameMixin, RedditBase):
3838

3939
STR_FIELD = "id"
4040

41-
@property
42-
def _kind(self) -> str:
43-
"""Return the object's kind shortcode."""
44-
return "ann"
45-
4641
@staticmethod
4742
def _parse_iso8601(value: str) -> datetime:
4843
# ``datetime.fromisoformat`` only accepts the ``Z`` suffix on Python 3.11+.
4944
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone()
5045

46+
@property
47+
def _kind(self) -> str:
48+
"""Return the object's kind shortcode."""
49+
return "ann"
50+
5151
@property
5252
def read_datetime(self) -> datetime | None:
5353
"""Return the time the announcement was read as a timezone-aware :class:`datetime.datetime`.

asyncpraw/models/reddit/collections.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -459,15 +459,6 @@ class Collection(CreatedMixin, RedditBase):
459459

460460
_created_at_attribute = "created_at_utc"
461461

462-
@property
463-
def updated_datetime(self) -> datetime.datetime:
464-
"""Return the last update time as a timezone-aware :class:`datetime.datetime`.
465-
466-
The returned object is localized to the system's timezone.
467-
468-
"""
469-
return self._to_local_datetime(self.last_update_utc)
470-
471462
@cachedproperty
472463
def mod(self) -> CollectionModeration:
473464
"""Get an instance of :class:`.CollectionModeration`.
@@ -489,6 +480,15 @@ def mod(self) -> CollectionModeration:
489480
"""
490481
return CollectionModeration(self._reddit, self.collection_id)
491482

483+
@property
484+
def updated_datetime(self) -> datetime.datetime:
485+
"""Return the last update time as a timezone-aware :class:`datetime.datetime`.
486+
487+
The returned object is localized to the system's timezone.
488+
489+
"""
490+
return self._to_local_datetime(self.last_update_utc)
491+
492492
def __init__(
493493
self,
494494
reddit: asyncpraw.Reddit,

0 commit comments

Comments
 (0)