Skip to content
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

Import from typing, typing_extensions and enum modules on Python 3.8 #2446

Merged
merged 18 commits into from
Apr 20, 2024

Conversation

jwortmann
Copy link
Member

On Python 3.8 enum and typing are part of the standard library.

.core.typing still contains all the fake typing classes, because many LSP-* plugins import from LSP.plugin.core.typing.

predragnikolic and others added 12 commits February 11, 2024 23:04
This reverts commit c70cd57.
* main:
  Cut 1.29.0
  Fix usage of sublime.score_selector (sublimelsp#2427)
  docs: add info about typst-lsp commands (sublimelsp#2424)
This commit adopts `unittesting.ViewTestCase` to run view related test cases.

ViewTestCase ...

1. provides `view` and `window` members pointing to a dedicated view, being
   created for testing.
2. ensures not to close empty windows, which was probably the most likely
   reason for unittests failing before on MacOS.
@jfcherng
Copy link
Contributor

jfcherng commented Apr 13, 2024

Fwiw, with from __future__ import annotations, type annotations are just ignored at runtime. People can use something like tuple[str, ...] | dict[str, Any] | None for type annotations in py38. Most of from typing import ... can be removed (later, or maybe not interested). ruff can do this automatically for a whole project but it's unsafe if there is one used outside type annotation.

@jwortmann
Copy link
Member Author

I would say that we can make those changes step by step in separate PRs. So the next steps could be

  • use modern type union syntax
    • replace Union[Foo, Bar] with Foo | Bar
    • replace Optional[Foo] with Foo | None
  • replace generic typing classes like Dict, List, Tuple with builtins dict, list, tuple

The pipe operator was introduced in Python 3.10 and the syntax with dict etc. in Python 3.9, so we would need to have from __future__ import annotations in all files.

And this is probably significantly more work if done manually throughout the whole code base, so we should check how well it would work to do it automatically.

@jwortmann
Copy link
Member Author

By the way, with this PR there are still 3 classes required to be imported from the local .core.typings, because they were introduced after Python 3.8: TypeGuard, NotRequired, and ParamSpec.
I've noticed that typing_extensions is on Package Control (https://github.com/packagecontrol/channel/blob/6faa8ffb43475f829ee95936955a962cce47d6d3/repository.json#L1387), so I wonder whether we could use that to avoid those 3 remaining imports.

@predragnikolic
Copy link
Member

Nice!

I would rather not rush into merging this in feat/py38,
I hope that you are OK if this PR is merged sometime after the 19th April? :)

@jwortmann
Copy link
Member Author

Okay, I can change the target branch then and rebase.


Regarding typing_extensions, I can see that it was merged two weeks ago in packagecontrol/channel@cd8ec01, but it's not (yet?) in the https://packagecontrol.github.io/channel/channel_v4.json file used by Package Control. So maybe we need to wait a bit until it gets updated.

I think with from __future__ import annotations we could alternatively also do

if TYPE_CHECKING:
    from typing import NotRequired, TypeGuard

but it wouldn't work with ParamSpec because that one is used outside of type annotations.

@predragnikolic
Copy link
Member

Will check the typing_extensions ,
if it works, then NotRequired, TypeGuard could be imported from there without the if TYPE_CHECKING: check... I think.

@jwortmann
Copy link
Member Author

jwortmann commented Apr 16, 2024

typing_extensions seems to be fixed upstream now. So the question would be if we want to have it as an additional dependency or not. With it, NotRequired, TypeGuard and ParamSpec can all be imported directly.

I missed to mention above that there is still StrEnum (used in protocol.py) that we need to import from .core.typings, but this is regardless whether typing_extensions is available or not.

@jfcherng
Copy link
Contributor

jfcherng commented Apr 16, 2024

I think more or less, StrEnum is just

class StrEnum(str, Enum):
    def __str__(self) -> str:
        return self.value.__str__()

@jwortmann
Copy link
Member Author

jwortmann commented Apr 16, 2024

There is another problem with LSP's fake enums. Currently we can use the syntax SomeEnum.SomeValue to get the value, because SomeEnum as an derived class from the fake enum is just a regular class, so we can access the attribute value directly. But if we import from the enum module, SomeEnum now is a real enum and SomeEnum.SomeValue would give something like <SomeEnum.SomeValue: 42> instead of just 42. In other words we would need to use SomeEnum.SomeValue.value instead throughout the code.

I guess I'll revert using the "real" enums, for now...

@jwortmann jwortmann changed the title Import from enum and typing modules on Python 3.8 Import from typing and typing_extensions modules on Python 3.8 Apr 16, 2024
@jfcherng
Copy link
Contributor

jfcherng commented Apr 16, 2024

There is another problem with LSP's fake enums. Currently we can use the syntax SomeEnum.SomeValue to get the value, because SomeEnum as an derived class from the fake enum is just a regular class, so we can access the attribute value directly. But if we import from the enum module, SomeEnum now is a real enum and SomeEnum.SomeValue would give something like <SomeEnum.SomeValue: 42> instead of just 42. In other words we would need to use SomeEnum.SomeValue.value instead throughout the code.

Maybe you can try

from enum import IntEnum


class Color(IntEnum):
    RED = 10

    def __str__(self) -> str:
        return self.value.__str__()

In ST console, it works. But I am not sure about other APIs.

>>> from enum import IntEnum


class Color(IntEnum):
    RED = 10
>>> Color.RED
<Color.RED: 10>
>>> r = sublime.Region(Color.RED)
>>> r
Region(10, 10)

ref: https://docs.python.org/3.12/library/enum.html#enum.IntEnum

@jwortmann
Copy link
Member Author

Maybe you can try [...]

The IntEnum classes are actually not problematic, because comparisons work out of the box for them. But a problem is that some other classes in protocol.py inherit from Enum with their values being strings. Unfortunately you can't compare via SomeEnum.SomeValue == 'something' if SomeEnum is just an Enum. But it works if it is a StrEnum.

And there are some problems in the initialize request, where we access the enum members directly in the code (and not via .value suffix). But I'll push a fix for that.

@jfcherng
Copy link
Contributor

jfcherng commented Apr 16, 2024

Maybe you can try [...]

The IntEnum classes are actually not problematic, because comparisons work out of the box for them. But a problem is that some other classes in protocol.py inherit from Enum with their values being strings. Unfortunately you can't compare via SomeEnum.SomeValue == 'something' if SomeEnum is just an Enum. But it works if it is a StrEnum.

#2446 (comment) works in my test.

>>> from enum import Enum


class StrEnum(str, Enum):
    def __str__(self) -> str:
        return self.value.__str__()


class Kind(StrEnum):
    KEYWORD = "1"
    CONSTANT = "2"


>>> Kind.KEYWORD == '1'
True
>>> Kind.KEYWORD < '2'
True

Would it be problematic if it inherits from this class StrEnum(str, Enum) rather than Enum? And replace

class StrEnum(Type): # type: ignore
pass
with

class StrEnum(str, Enum):
    def __str__(self) -> str:
        return self.value.__str__()

@jwortmann
Copy link
Member Author

Would it be problematic if it inherits from this class StrEnum(str, Enum) rather than Enum?

No, your suggestion should work too. But it's not much different in practice from what we currently have with

class StrEnum:
    pass

The problem was that the enums in protocol.py are automatically generated from https://github.com/microsoft/lsprotocol and they used Enum instead of StrEnum. The PR linked somewhere above should workaround that.

@jfcherng
Copy link
Contributor

jfcherng commented Apr 16, 2024

But then do we still have Enum, IntEnum or StrEnum issues? What are them?

@jwortmann
Copy link
Member Author

Problem 1 was that for the initialize request we used for example

"valueSet": [InsertTextMode.AdjustIndentation]

which would give us something like [<InsertTextMode.AdjustIndentation: 1>] (which is not JSON serializable) if we import IntEnum from the enum module instead of using a fake class.

Problem 2 was that comparisons of enums which have string values would not work anymore in the code, because those enums only used the Enum class and not StrEnum.

But both should be fixed now in 8c01147

@jwortmann jwortmann changed the title Import from typing and typing_extensions modules on Python 3.8 Import from typing, typing_extensions and enum modules on Python 3.8 Apr 16, 2024
@predragnikolic predragnikolic deleted the branch sublimelsp:main April 19, 2024 16:14
@jwortmann
Copy link
Member Author

@predragnikolic I wonder can you restore the feat/py38 branch temporarily? Otherwise it seems to be impossible to change the target branch of this PR

@predragnikolic
Copy link
Member

Will do, I saw what happened.
I didn't expect that.

I thought that it would just switch to the main branch.

@jwortmann jwortmann changed the base branch from feat/py38 to main April 19, 2024 17:39
@jwortmann
Copy link
Member Author

Ok I think now the feat/py38 branch can be deleted.

And I think we discovered a bug in GitHub's UI 😆

github

@predragnikolic
Copy link
Member

predragnikolic commented Apr 19, 2024

Well, I died a bit inside when I saw this :D

@predragnikolic predragnikolic merged commit c5321ad into sublimelsp:main Apr 20, 2024
8 checks passed
@jwortmann jwortmann deleted the typing branch April 20, 2024 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants