Skip to content

Hover on property with setter during assignment shows nothing #5218

Description

@StefanBRas

Describe the Bug

When sending the hover lsp command on a property with a setter that is being assigned to it doesn't show anything. When not assigning it correctly shows the type and similarly for non-property values it shows it

class A:
    def __init__(self) -> None:
        self._x: int | None = None

    @property
    def x(self) -> int | None:
        return self._x

    @x.setter
    def x(self, value: int | None) -> None:
        self._x = value

    def meth(self) -> None:
        self.x = 1 # ERR! Hover on `x` returns nothing ("no information available" in neovim). Should show `int | None`
        self.x # OK! Hover correctly show int | None
        self._x = 1 # OK! Hover on `_x` correctly shows Literal[1]

a = A()
a.x = 1 # ERR! Hover on `x` returns nothing ("no information available" in neovim). Should show `int | None`
a.x # OK! Hover correctly show int | None
a._x = 1 # OK! Hover on `_x` correctly shows Literal[1]

Sandbox Link

https://pyrefly.org/sandbox/?project=v2.vVVNC4JAEP0rUx6si0vQqVMdgqBDUceEXEoyKEdcK_v3zbjqYn4gHbqK7-17zLw3bZWpo7JoC0lrKo7pjMbyta1FOuZRjBEV2duwpobOwOpLmHOXRKmj_ISK65uoDK8h65Dalt5uiwybgAXL3W4AK-5hoGl7qZeLVUADDPjWjNxhiCSFFvYu9XI95TVrYHfI2xv6-Lzexw7sA3zczqACfIFntHu1py3YrItXTxjzAt3eGmdgTUZZsYGy4CMprlKo4rIdJllfSsItOHPyz6b5vZ5O5W_26um1YKs_QB4EQlPsLw-6juTCcUOLDqMPQZJEaiZEAcf4IvxQnPGkRAUhgIuqNF-lA8zaRhFtc21Me9WGrVvD7ioNmzuD_ujsgQ8

(Only applicable for extension issues) IDE Information

No response

Activity

  1. StefanBRas commented on Oct 13, 2026

    @StefanBRas
    Author

    For reference ty shows the just assigned type, which I (and the submitter of this issue) finds incorrect

  2. Aayushyaash commented on Oct 14, 2026

    @Aayushyaash
    Contributor

    Hi @StefanBRas,

    I was hoping to help solve this issue, so I dug into the codebase to see how this is currently handled.

    I noticed there's actually a test in write_target_type.rs with a comment explaining why it behaves this way right now:

    "Writing through a property, a descriptor or __setattr__ runs a setter, which produces no type Pyrefly can attribute to the target, so the target reports nothing. In particular the getter is never consulted: reading p.p and d.d gives int, which must not leak into the store."

    So returning None was an intentional decision at the time to make sure getter types wouldn't leak into write targets (and to avoid what tools like Pyright and ty do, where they attach the RHS value type to the attribute).

    That said, showing nothing isn't a solution.

    I was thinking about what we could do here. Whenever someone hovers over an attribute in an assignment, if it resolves to a property with a setter, we could look up the setter method and display its parameter type.

    Here is how both symmetric and asymmetric property would look before and after:

    # Example 1: Symmetric Property
    class A:
        def __init__(self) -> None:
            self._x: int | None = None
    
        @property
        def x(self) -> int | None:
            return self._x
    
        @x.setter
        def x(self, value: int | None) -> None:
            self._x = value
    
    a = A()
    # Before: Hover on `x` returns None ("no information available")
    # After:  Hover on `x` returns `(attribute) x: int | None`
    a.x = 1
    
    
    # Example 2: Asymmetric Property
    class B:
        @property
        def items(self) -> tuple[int, ...]:
            return self._items
    
        @items.setter
        def items(self, value: Iterable[int]) -> None:
            self._items = tuple(value)
    
    b = B()
    # Before: Hover on `items` returns None
    # After:  Hover on `items` returns `(attribute) x: Iterable[int]` (the setter's input contract)
    b.items = [1, 2]

    This way, we provide useful hover information on assignments while ensuring we never leak the getter type or show the RHS literal.

    Do you think this makes sense, or did you have a different behavior in mind? If this sounds good to you and the maintainers, I'd love to claim the issue and work on a PR.

  3. StefanBRas commented on Oct 15, 2026

    @StefanBRas
    Author

    I think for me as a user, what would help the most would be to show that this is a property where the setter and getter differs in type. Even when they don't differ I think it's nice to know that it has a custom setter, because it can be surprising that assigning to a variable actually runs a function.

    From a quick read of the LSP spec on HoverRequest there's no restrictions on what you're allowed to show so I don't think there's anything blocking from showing the full information

  4. added theissue type on Nov 5, 2026
  5. rchen152 commented on Nov 5, 2026

    @rchen152
    Contributor

    Maybe we could use the getter type when the property is symmetric and print the property's getter and setter when it's asymmetric.

  6. Aayushyaash commented on Nov 6, 2026

    @Aayushyaash
    Contributor

    Showing both getter and setter types for asymmetric properties makes a lot of sense.

    Here’s a quick before & after of what this would look like for reads vs writes:


    1. Symmetric property (getter == setter)

    class A:
        def __init__(self) -> None:
            self._x: int | None = None
    
        @property
        def x(self) -> int | None:
            return self._x
    
        @x.setter
        def x(self, value: int | None) -> None:
            self._x = value
    
    a = A()
    val = a.x   # Read
    a.x = 1     # Write
    • Read (val = a.x): (property) x: int | None (unchanged)
    • Write (a.x = 1):
      • Before: None
      • After: (property) x: int | None

    2. Asymmetric property (getter != setter)

    from collections.abc import Iterable
    
    class B:
        def __init__(self) -> None:
            self._items: tuple[int, ...] = ()
    
        @property
        def items(self) -> tuple[int, ...]:
            return self._items
    
        @items.setter
        def items(self, value: Iterable[int]) -> None:
            self._items = tuple(value)
    
    b = B()
    res = b.items     # Read
    b.items = [1, 2]  # Write
    • Read (res = b.items): (property) items: tuple[int, ...] (unchanged)
    • Write (b.items = [1, 2]):
      • Before: None

      • After:

        (property) items: Iterable[int]
        
        Property Accessors:
          Setter: Iterable[int] (assigned here)
          Getter: tuple[int, ...]
        

    The hover displays the setter's type first since that's what the assignment accepts, with the getter listed below for context.


    Custom Descriptors (__get__ / __set__)

    In write_target_type.rs (test_property_and_descriptor_target) and attr.rs, AttrSetter::DescriptorSetter is handled alongside PropertySetter and currently has the same None target behavior.

    Since Pyrefly already has get_descriptor_setter_value() for extracting the __set__ value parameter, I can extend the implementation to cover custom descriptors in the same PR.

  7. Aayushyaash commented on Nov 6, 2026

    @Aayushyaash
    Contributor

    I'll start working on this and open a PR. #claim

  8. github-actions commented on Nov 6, 2026

    @github-actions

    @Aayushyaash you've claimed this issue — it's now assigned to you. Thanks for picking it up! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

has-open-prlanguage-serverIssues specific to our IDE integration rather than type checking

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions