Skip to content
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
c68ede8
introduce replace_item and some additional patches
NeloBlivion Dec 23, 2025
f757ac0
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 23, 2025
56b5b46
cls
NeloBlivion Dec 23, 2025
ec0ddfa
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 23, 2025
deeed74
rework underlying
NeloBlivion Dec 23, 2025
c085789
Merge branch 'master' into cv2_fixes
NeloBlivion Dec 23, 2025
130fab8
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 23, 2025
45da8fe
maybe fixed
NeloBlivion Dec 23, 2025
aadf0ed
,
NeloBlivion Dec 23, 2025
3289505
or
NeloBlivion Dec 23, 2025
d984274
spacing
NeloBlivion Dec 23, 2025
97db5d4
replace and remove on gallery
NeloBlivion Dec 23, 2025
3775262
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 23, 2025
a5d076c
index
NeloBlivion Dec 23, 2025
22aaeb1
select_type
NeloBlivion Dec 23, 2025
d06f364
row
NeloBlivion Dec 23, 2025
6906ae5
type
NeloBlivion Dec 23, 2025
d993efe
cl
NeloBlivion Dec 24, 2025
3fc8b2a
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 24, 2025
cb403d2
fix(actions): rework release workflow (#3034)
Lulalaby Dec 24, 2025
7d65cf4
Merge branch 'master' into cv2_fixes
Paillat-dev Dec 24, 2025
5bfee91
Merge branch 'master' into cv2_fixes
Lulalaby Dec 24, 2025
d16f857
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 24, 2025
8f048e1
Merge branch 'master' into cv2_fixes
Lulalaby Dec 24, 2025
1074d51
Merge branch 'master' into cv2_fixes
Lulalaby Dec 24, 2025
91390cb
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 24, 2025
2f48c7b
revert cl
NeloBlivion Dec 24, 2025
ea33a62
files
NeloBlivion Dec 24, 2025
2ba67c3
file again
NeloBlivion Dec 24, 2025
3c49f09
one more
NeloBlivion Dec 24, 2025
738b6ee
Merge branch 'master' into cv2_fixes
NeloBlivion Dec 24, 2025
1ae6835
cl
NeloBlivion Dec 24, 2025
eb9fa92
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 24, 2025
2488e63
buildout for new features & items aliases
NeloBlivion Dec 25, 2025
5ebdeaa
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 25, 2025
050f639
fix
NeloBlivion Dec 25, 2025
d831318
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 25, 2025
6eb6086
NeloBlivion Dec 25, 2025
6496c4c
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 25, 2025
de61d8e
fix modal typing
NeloBlivion Dec 29, 2025
718fe73
Merge branch 'master' into cv2_fixes
NeloBlivion Dec 29, 2025
bc785f4
Merge branch 'master' into cv2_fixes
NeloBlivion Dec 29, 2025
66a4db6
correct return types
NeloBlivion Dec 29, 2025
de42bb6
fix modal error docs
NeloBlivion Dec 29, 2025
c4000cb
remove incorrect release script
NeloBlivion Dec 29, 2025
2f3da73
Merge branch 'master' into cv2_fixes
NeloBlivion Jan 4, 2026
c7a3983
fix paginator
NeloBlivion Jan 4, 2026
5e02950
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 4, 2026
572fe5a
doc fix
NeloBlivion Jan 8, 2026
d9b9c1f
add convenience methods to DesignerView
NeloBlivion Jan 8, 2026
a054102
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 8, 2026
9adf32d
Merge branch 'master' into cv2_fixes
Dorukyum Jan 12, 2026
2728982
adjust underlying order
NeloBlivion Jan 17, 2026
534f743
fix fileupload
NeloBlivion Jan 17, 2026
7cf34bd
Merge branch 'master' into cv2_fixes
NeloBlivion Jan 17, 2026
9db2632
misc
NeloBlivion Jan 17, 2026
d6e287d
view.add_row
NeloBlivion Jan 17, 2026
eba1d9e
Merge branch 'master' into cv2_fixes
NeloBlivion Jan 19, 2026
4dccef7
Merge branch 'Pycord-Development:master' into cv2_fixes
NeloBlivion Jan 21, 2026
6e5507a
misc fixes
NeloBlivion Jan 23, 2026
01e95f8
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#2956](https://github.com/Pycord-Development/pycord/pull/2956))
- Added `Guild.fetch_roles_member_counts` method and `GuildRoleCounts` class.
([#3020](https://github.com/Pycord-Development/pycord/pull/3020))
- Added `replace_item` to `DesignerView`, `Section`, `Container`, `ActionRow`, &
`MediaGallery` ([#3032](https://github.com/Pycord-Development/pycord/pull/3032))

### Changed

Expand All @@ -33,6 +35,8 @@ These changes are available on the `master` branch, but have not yet been releas
TypeVars. ([#3002](https://github.com/Pycord-Development/pycord/pull/3002))
- Fixed `View`'s `disable_on_timeout` not working in private (DM) channels.
([#3016](https://github.com/Pycord-Development/pycord/pull/3016))
- Fixed core issues with modifying items in `Container` and `Section`
([#3032](https://github.com/Pycord-Development/pycord/pull/3032))

### Deprecated

Expand Down
12 changes: 12 additions & 0 deletions discord/colour.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ def to_rgb(self) -> tuple[int, int, int]:
"""Returns an (r, g, b) tuple representing the colour."""
return self.r, self.g, self.b

@classmethod
def resolve_value(cls: type[CT], value: int | Colour | None) -> CT:
if value is None or isinstance(value, Colour):
return value
elif isinstance(value, int):
return cls(value=value)
else:
raise TypeError(
"Expected discord.Colour, int, or None but received"
f" {value.__class__.__name__} instead."
)

@classmethod
def from_rgb(cls: type[CT], r: int, g: int, b: int) -> CT:
"""Constructs a :class:`Colour` from an RGB tuple."""
Expand Down
2 changes: 1 addition & 1 deletion discord/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def _raw_construct(cls: type[C], **kwargs) -> C:
try:
value = kwargs[slot]
except KeyError:
pass
setattr(self, slot, None)
else:
setattr(self, slot, value)
return self
Expand Down
10 changes: 1 addition & 9 deletions discord/embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,15 +522,7 @@ def colour(self) -> Colour | None:

@colour.setter
def colour(self, value: int | Colour | None): # type: ignore
if value is None or isinstance(value, Colour):
self._colour = value
elif isinstance(value, int):
self._colour = Colour(value=value)
else:
raise TypeError(
"Expected discord.Colour, int, or None but received"
f" {value.__class__.__name__} instead."
)
self._colour = Colour.resolve_value(value)

color = colour

Expand Down
57 changes: 47 additions & 10 deletions discord/ui/action_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,7 @@ def __init__(

self.children: list[ViewItem] = []

self._underlying = ActionRowComponent._raw_construct(
type=ComponentType.action_row,
id=id,
children=[],
)
self._underlying = self._generate_underlying(id=id)

for func in self.__row_children_items__:
item: ViewItem = func.__discord_ui_model_type__(
Expand All @@ -112,13 +108,24 @@ def __init__(
self.add_item(i)

def _add_component_from_item(self, item: ViewItem):
self._underlying.children.append(item._underlying)
self.underlying.children.append(item._generate_underlying())

def _set_components(self, items: list[ViewItem]):
self._underlying.children.clear()
self.underlying.children.clear()
for item in items:
self._add_component_from_item(item)

def _generate_underlying(self, id: int | None = None) -> ActionRowComponent:
super()._generate_underlying(ActionRowComponent)
row = ActionRowComponent._raw_construct(
type=ComponentType.action_row,
id=id or self.id,
children=[],
)
for i in self.children:
row.children.append(i._generate_underlying())
return row

def add_item(self, item: ViewItem) -> Self:
"""Adds an item to the action row.

Expand Down Expand Up @@ -164,6 +171,36 @@ def remove_item(self, item: ViewItem | str | int) -> Self:
item.parent = None
return self

def replace_item(
self, original_item: ViewItem | str | int, new_item: ViewItem
) -> Self:
"""Directly replace an item in this row.
If an :class:`int` is provided, the item will be replaced by ``id``, otherwise by ``custom_id``.

Parameters
----------
original_item: Union[:class:`ViewItem`, :class:`int`, :class:`str`]
The item, item ``id``, or item ``custom_id`` to replace in the row.
new_item: :class:`ViewItem`
The new item to insert into the row.
"""

if not isinstance(new_item, (Select, Button)):
raise TypeError(f"expected Select or Button, not {new_item.__class__!r}")

if isinstance(original_item, (str, int)):
original_item = self.get_item(original_item)
if not original_item:
raise ValueError(f"Could not find original_item in row.")
try:
i = self.children.index(original_item)
new_item.parent = self
self.children[i] = new_item
original_item.parent = None
except ValueError:
raise ValueError(f"Could not find original_item in row.")
return self

def get_item(self, id: str | int) -> ViewItem | None:
"""Get an item from this action row. Roughly equivalent to `utils.get(row.children, ...)`.
If an ``int`` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``.
Expand Down Expand Up @@ -358,7 +395,7 @@ def is_persistent(self) -> bool:
return all(item.is_persistent() for item in self.children)

def refresh_component(self, component: ActionRowComponent) -> None:
self._underlying = component
self.underlying = component
for i, y in enumerate(component.components):
x = self.children[i]
x.refresh_component(y)
Expand Down Expand Up @@ -396,14 +433,14 @@ def width(self):
"""Return the sum of the items' widths."""
t = 0
for item in self.children:
t += 1 if item._underlying.type is ComponentType.button else 5
t += 1 if item.underlying.type is ComponentType.button else 5
return t

def walk_items(self) -> Iterator[ViewItem]:
yield from self.children

def to_component_dict(self) -> ActionRowPayload:
self._set_components(self.children)
self._underlying = self._generate_underlying()
return super().to_component_dict()

@classmethod
Expand Down
63 changes: 43 additions & 20 deletions discord/ui/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,7 @@ def __init__(
f" {emoji.__class__}"
)

self._underlying = ButtonComponent._raw_construct(
type=ComponentType.button,
self._underlying = self._generate_underlying(
custom_id=custom_id,
url=url,
disabled=disabled,
Expand All @@ -160,76 +159,100 @@ def __init__(
)
self.row = row

def _generate_underlying(
self,
style: ButtonStyle | None = None,
label: str | None = None,
disabled: bool = False,
custom_id: str | None = None,
url: str | None = None,
emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None,
sku_id: int | None = None,
id: int | None = None,
) -> ButtonComponent:
super()._generate_underlying(ButtonComponent)
return ButtonComponent._raw_construct(
type=ComponentType.button,
custom_id=custom_id or self.custom_id,
url=url or self.url,
disabled=disabled or self.disabled,
label=label or self.label,
style=style or self.style,
emoji=emoji or self.emoji,
sku_id=sku_id or self.sku_id,
id=id or self.id,
)

@property
def style(self) -> ButtonStyle:
"""The style of the button."""
return self._underlying.style
return self.underlying.style

@style.setter
def style(self, value: ButtonStyle):
self._underlying.style = value
self.underlying.style = value

@property
def custom_id(self) -> str | None:
"""The ID of the button that gets received during an interaction.

If this button is for a URL, it does not have a custom ID.
"""
return self._underlying.custom_id
return self.underlying.custom_id

@custom_id.setter
def custom_id(self, value: str | None):
if value is not None and not isinstance(value, str):
raise TypeError("custom_id must be None or str")
if value and len(value) > 100:
raise ValueError("custom_id must be 100 characters or fewer")
self._underlying.custom_id = value
self.underlying.custom_id = value
self._provided_custom_id = value is not None

@property
def url(self) -> str | None:
"""The URL this button sends you to."""
return self._underlying.url
return self.underlying.url

@url.setter
def url(self, value: str | None):
if value is not None and not isinstance(value, str):
raise TypeError("url must be None or str")
self._underlying.url = value
self.underlying.url = value

@property
def disabled(self) -> bool:
"""Whether the button is disabled or not."""
return self._underlying.disabled
return self.underlying.disabled

@disabled.setter
def disabled(self, value: bool):
self._underlying.disabled = bool(value)
self.underlying.disabled = bool(value)

@property
def label(self) -> str | None:
"""The label of the button, if available."""
return self._underlying.label
return self.underlying.label

@label.setter
def label(self, value: str | None):
if value and len(str(value)) > 80:
raise ValueError("label must be 80 characters or fewer")
self._underlying.label = str(value) if value is not None else value
self.underlying.label = str(value) if value is not None else value

@property
def emoji(self) -> PartialEmoji | None:
"""The emoji of the button, if available."""
return self._underlying.emoji
return self.underlying.emoji

@emoji.setter
def emoji(self, value: str | GuildEmoji | AppEmoji | PartialEmoji | None): # type: ignore
if value is None:
self._underlying.emoji = None
self.underlying.emoji = None
elif isinstance(value, str):
self._underlying.emoji = PartialEmoji.from_str(value)
self.underlying.emoji = PartialEmoji.from_str(value)
elif isinstance(value, _EmojiTag):
self._underlying.emoji = value._to_partial()
self.underlying.emoji = value._to_partial()
else:
raise TypeError(
"expected str, GuildEmoji, AppEmoji, or PartialEmoji, received"
Expand All @@ -239,14 +262,14 @@ def emoji(self, value: str | GuildEmoji | AppEmoji | PartialEmoji | None): # ty
@property
def sku_id(self) -> int | None:
"""The ID of the SKU this button refers to."""
return self._underlying.sku_id
return self.underlying.sku_id

@sku_id.setter
def sku_id(self, value: int | None): # type: ignore
if value is None:
self._underlying.sku_id = None
self.underlying.sku_id = None
elif isinstance(value, int):
self._underlying.sku_id = value
self.underlying.sku_id = value
else:
raise TypeError(f"expected int or None, received {value.__class__} instead")

Expand Down Expand Up @@ -281,7 +304,7 @@ def is_persistent(self) -> bool:
return super().is_persistent()

def refresh_component(self, button: ButtonComponent) -> None:
self._underlying = button
self.underlying = button


def button(
Expand Down
Loading
Loading