Skip to content

merge networkx Graph classes from python-type-stubs and address a few recent issues #14597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Avasam
Copy link
Collaborator

@Avasam Avasam commented Aug 20, 2025

Extracted from #14038 with a few additional changes. Should reduce changes in that PR

Changes:

There seems to have been a bit on confusion regarding these stubs recently, let's take the time to properly review these changes, and discuss any uncertainty (whilst I do think this PR is correct, I could be wrong)

CC @srittau @TomerGodinger

This comment has been minimized.

Comment on lines +88 to +93
# Overriden in __init__ to always raise
def add_edge(self, u_of_edge: _Node, v_of_edge: _Node, **attr: Unused) -> NoReturn: ...
def add_edges_from(self, ebunch_to_add: Iterable[_EdgePlus[_Node]], **attr: Unused) -> NoReturn: ...
def add_weighted_edges_from(
self, ebunch_to_add: Iterable[tuple[_Node, _Node, float]], weight: str = "weight", **attr: Unused
) -> NoReturn: ...
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't strictly necessary and I don't mind removing it from the PR if unwanted

This comment has been minimized.

@Avasam Avasam requested a review from charmoniumQ August 20, 2025 18:40
Comment on lines 59 to 64
def __call__(self, nbunch: None = None, weight: None | bool | str = None) -> int: ... # type: ignore[overload-overlap]
def __call__(self, nbunch: None = None, weight: None | bool | str = None) -> Self: ...
@overload
def __call__(self, nbunch: None | Iterable[_Node], weight: None | bool | str = None) -> Self: ...
def __call__(self, nbunch: Iterable[_Node], weight: None | bool | str = None) -> int: ...
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#14595 (comment)

the overload should be specialized to _Node.

None and Iterable[_Node] return Self (first and last return), but _Node returns int (middle two returns)

(ignoring, like we have been, that it could actually return float in the case of graphs whose edge weights are float)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, Iterable[_Node] is typed as returning int, but I think it should return Self (like None), whereas _Node should return int.

They didn't design this API with typechecking in mind.

Copy link
Collaborator Author

@Avasam Avasam Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ignoring, like we have been, that it could actually return float in the case of graphs whose edge weights are float)

Considering we typed __getitem__ to return float, it'd be more consistent to return float from self[nbunch] (unless you have a specific reason to not do that)

For reference to future readers, the __call__ method:

    def __call__(self, nbunch=None, weight=None):
        if nbunch is None:
            if weight == self._weight:
                return self
            return self.__class__(self._graph, None, weight)
        try:
            if nbunch in self._nodes:
                if weight == self._weight:
                    return self[nbunch]
                return self.__class__(self._graph, None, weight)[nbunch]
        except TypeError:
            pass
        return self.__class__(self._graph, nbunch, weight)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll also flip the order of overloads in case the hashable node is a str (because of the whole str matches Iterable[str] issue)

Copy link
Contributor

@charmoniumQ charmoniumQ Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be more consistent to return float from self[nbunch] (unless you have a specific reason to not do that)

I was thinking it might break code that does calculation on the degree of nodes, which is always an int. But we have mypy_primer to tell us if that will actually break any code or not.

because of the whole str matches Iterable[str] issue

Ugh, don't remind me /s

Good catch though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They didn't design this API with typechecking in mind.

Tbf most libraries older than a few years haven't ^^ Especially powerful ones actively using dynamic typing.

But I think we're able to represent this as you've mentioned. My last commit should do that.

Copy link
Collaborator Author

@Avasam Avasam Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be more consistent to return float from self[nbunch] (unless you have a specific reason to not do that)

I was thinking it might break code that does calculation on the degree of nodes, which is always an int. But we have mypy_primer to tell us if that will actually break any code or not.

Looking at the implementation, Pylance/pyright already infers that __getitem__ and __call__ always returns int. That's another good catch.

image

it could be because int is the fallback to sum with Unknown. But if a DiDegreeView is always meant to be working on degrees, which would be an int, then yeah it should return int

Copy link
Collaborator Author

@Avasam Avasam Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we have mypy_primer to tell us if that will actually break any code or not.

Unfortunately mypy_primer has a ton of false-negative, because it doesn't force re-enable anything (last I checked).
So if a project excludes files, disables codes, doesn't explicitly checks untyped methods, or doesn't run in strict mode, then the primer won't catch any of those diffs since mypy didn't run on that code (or with the disabled rules)

For instance I've been fixing tons of issues in pywin32 and setuptools 's own projects referencing the stubs from typeshed, and I almost never see the primer telling me I've fixed stuff, because the violations I'm fixing are disabled in said projects due to too many false-positives.

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

discord.py (https://github.com/Rapptz/discord.py)
- discord/ext/commands/hybrid.py:508: error: Overlap between argument names and ** TypedDict items: "description", "name"  [misc]
+ discord/ext/commands/hybrid.py:508: error: Overlap between argument names and ** TypedDict items: "name", "description"  [misc]
- discord/ext/commands/hybrid.py:629: error: Overlap between argument names and ** TypedDict items: "description", "name"  [misc]
+ discord/ext/commands/hybrid.py:629: error: Overlap between argument names and ** TypedDict items: "name", "description"  [misc]

Comment on lines +61 to +66
@overload # Use this overload first in case _Node=str, since `str` matches `Iterable[str]`
def __call__(self, nbunch: _Node, weight: None | bool | str = None) -> int: ... # type: ignore[overload-overlap]
@overload
def __call__(self, nbunch: None = None, weight: None | bool | str = None) -> int: ... # type: ignore[overload-overlap]
@overload
def __call__(self, nbunch: None | Iterable[_Node], weight: None | bool | str = None) -> Self: ...
def __getitem__(self, n: _Node) -> float: ...
def __iter__(self) -> Iterator[tuple[_Node, float]]: ...
def __call__(self, nbunch: Iterable[_Node] | None = None, weight: None | bool | str = None) -> Self: ...
def __getitem__(self, n: _Node) -> int: ...
def __iter__(self) -> Iterator[tuple[_Node, int]]: ...
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've included this file because of #14595 (comment)

But if reviewer is uncertain about these changes, I can split it off

@@ -55,12 +58,12 @@ class NodeDataView(AbstractSet[_Node]):

class DiDegreeView(Generic[_Node]):
def __init__(self, G: Graph[_Node], nbunch: _NBunch[_Node] = None, weight: None | bool | str = None) -> None: ...
@overload # Use this overload first in case _Node=str, since `str` matches `Iterable[str]`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is worth a test in check_tricky_function_params.py?

Copy link
Collaborator Author

@Avasam Avasam Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. I was also pondering if I should add a test or if a comment is enough.

I guess both don't hurt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants