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

Expected class type but received "TokenMeta" #1562

Closed
alextremblay opened this issue Jul 15, 2021 · 10 comments
Closed

Expected class type but received "TokenMeta" #1562

alextremblay opened this issue Jul 15, 2021 · 10 comments

Comments

@alextremblay
Copy link

Hello all,

I'm trying to annotate a metaclass method which returns a new subclass of a "metaclass instance" (base class? would that be the right word for a class which uses this metaclass?)
And I'm running into the above error. Please see the code snippet for details

Environment data

  • Language Server version: 2021.7.2
  • OS and version: MacOS 10.15
  • Python version: 3.9.4

Code Snippet / Additional information

from __future__ import annotations
from typing import Callable, List, Tuple, Type, TypeVar, cast

class TokenMeta(type): 
    """Metaclass for dynamic attribute access on class (not on class instance)
    
    With any class you use this metaclass in, references to capital-case attributes 
    on that class will return named subclasses of that class
    
    see Token as an example"""
    
    def __getattr__(cls: TokenMeta, name: str) -> TokenMeta:
        if name[0].isupper():
            parent_class_name = cls.__name__
            new_subclass_name = f'{parent_class_name}.{name}'
            new_subclass = cast(
                TokenMeta,
                type(new_subclass_name, (cls,), {})
            )
            # register this subclass as an attribute on its parent class
            # next time it is referenced, that attribute will be used, and this method will not be called
            setattr(cls, name, new_subclass)

            return new_subclass
        #else:
        raise AttributeError

class Token(str, metaclass=TokenMeta):
    """subclass of string, used to categorize different types of strings. 
    referencing any capital-case attribute of this class returns a subclass named after that attribute
    
    Example:
        >>> token_string = Token('hello')
        >>> # all tokens and subclasses are, at the end of the day, strings.
        >>> isinstance(token_string, str)
        True
        >>> # You can define any categories you want
        >>> other_string = Token.Category('some string')
        >>> a_third_string = Token.OtherCategory('some other string')
        >>> # All categories are subclasses of Token (and by extension str)
        >>> issubclass(Token.Category, Token) and issubclass(Token.OtherCategory, Token)
        True
        >>> isinstance(other_string, Token.Category)
        True
        >>> isinstance(a_third_string, Token.Category)
        False
        >>> # Categories can be arbitrarily nested
        >>> yet_another_string = Token.Category.SubCategory('hello')
        >>> # string equality still works between them all
        >>> token_string == yet_another_string
        True
    """
    def __repr__(self,):
        return f'{self.__class__.__name__}({super().__repr__()})'

Expected behaviour

I expect pylance to recognize Token.Category as a subclass of Token

Actual behaviour

Pylance reports the listed error, and sees Token.Category as a TokenMeta instance, does not provide type completion for Token.Category

On the surface, this issue seems identical to #360, but that issue was reportedly fixed in 2020.9.5

Logs

Click to expand
[Info  - 9:21:50 p.m.] Pylance language server 2021.7.2 (pyright adb49aaa) starting
[Info  - 9:21:50 p.m.] Server root directory: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist
[Info  - 8:50:00 a.m.] No configuration file found.
[Info  - 8:50:00 a.m.] No pyproject.toml file found.
[Info  - 8:50:00 a.m.] Setting pythonPath for service "ga-experiments": "/usr/local/bin/python"
[Warn  - 8:50:00 a.m.] stubPath /Users/alex/Downloads/ga-experiments/typings is not a valid directory.
[Info  - 8:50:01 a.m.] Assuming Python version 3.9
[Info  - 8:50:01 a.m.] Assuming Python platform Darwin
Search paths for /Users/alex/Downloads/ga-experiments
  /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib
  /Users/alex/Downloads/ga-experiments
  /Users/alex/Downloads/ga-experiments/typings
  /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stubs/...
  /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/bundled/stubs
  /usr/local/Cellar/python@3.9/3.9.4/Frameworks/Python.framework/Versions/3.9/lib/python3.9
  /usr/local/Cellar/python@3.9/3.9.4/Frameworks/Python.framework/Versions/3.9/lib/python3.9/lib-dynload
  /Users/alex/Library/Python/3.9/lib/python/site-packages
  /usr/local/lib/python3.9/site-packages
  /Users/alex/Projects/python/yasha
[Info  - 8:50:01 a.m.] Searching for source files
[Info  - 8:50:01 a.m.] Found 2 source files
Background analysis message: setConfigOptions
Background analysis message: ensurePartialStubPackages
Background analysis message: setTrackedFiles
Background analysis message: markAllFilesDirty
Background analysis message: getSemanticTokens delta
[BG(1)] getSemanticTokens delta previousResultId:1626323995094 at /Users/alex/Downloads/ga-experiments/fetch.py ...
[BG(1)]   parsing: /Users/alex/Downloads/ga-experiments/fetch.py (82ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/builtins.pyi [fs read 15ms] (56ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/builtins.pyi (20ms)
[BG(1)]   binding: /Users/alex/Downloads/ga-experiments/fetch.py (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing.pyi [fs read 1ms] (15ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing.pyi (10ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/_typeshed/__init__.pyi [fs read 0ms] (4ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/_typeshed/__init__.pyi (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing_extensions.pyi [fs read 0ms] (3ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing_extensions.pyi (2ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/itertools.pyi [fs read 1ms] (3ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/itertools.pyi (2ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/subprocess.pyi [fs read 1ms] (9ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/subprocess.pyi (7ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/abc.pyi [fs read 1ms] (1ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/abc.pyi (2ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/os/__init__.pyi [fs read 1ms] (11ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/os/__init__.pyi (6ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/json/__init__.pyi [fs read 1ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/json/__init__.pyi (0ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/pathlib.pyi [fs read 0ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/pathlib.pyi (2ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/urllib/parse.pyi [fs read 1ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/urllib/parse.pyi (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/collections/__init__.pyi [fs read 0ms] (7ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/collections/__init__.pyi ...
[BG(1)]     parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/_collections_abc.pyi [fs read 1ms] (1ms)
[BG(1)]     binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/_collections_abc.pyi (0ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/collections/__init__.pyi (4ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/__init__.py [fs read 0ms] (3ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/__init__.py (0ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/diff.py [fs read 10ms] (55ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/diff.py (13ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/model.py [fs read 1ms] (7ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/model.py (6ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/helper.py [fs read 1ms] (11ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/helper.py (3ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/decimal.pyi [fs read 1ms] (5ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/decimal.pyi (3ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/logging/__init__.pyi [fs read 1ms] (9ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/logging/__init__.pyi (3ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/sys.pyi [fs read 0ms] (5ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/sys.pyi (2ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/io.pyi [fs read 1ms] (4ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/io.pyi (1ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/serialization.py [fs read 1ms] (14ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/serialization.py (2ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/distance.py [fs read 1ms] (3ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/distance.py (1ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/base.py [fs read 1ms] (1ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/base.py (0ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/parse.py [fs read 8ms] (41ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/parse.py (7ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/json/encoder.pyi [fs read 0ms] (1ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/json/encoder.pyi (0ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/json/decoder.pyi [fs read 0ms] (1ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/json/decoder.pyi (0ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/types.pyi [fs read 1ms] (4ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/types.pyi (2ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/difflib.pyi [fs read 0ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/difflib.pyi (0ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/math.pyi [fs read 1ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/math.pyi (0ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/ordered_set.py [fs read 19ms] (27ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/ordered_set.py (2ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/deephash.py [fs read 1ms] (7ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/deephash.py (3ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/warnings.pyi [fs read 0ms] (4ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/warnings.pyi (0ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/re.pyi [fs read 0ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/re.pyi (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/enum.pyi [fs read 0ms] (1ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/enum.pyi (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/copy.pyi [fs read 1ms] (1ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/copy.pyi (0ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/deepdiff/lfucache.py [fs read 0ms] (3ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/deepdiff/lfucache.py (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/functools.pyi [fs read 1ms] (3ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/functools.pyi (1ms)
[BG(1)] getSemanticTokens delta previousResultId:1626323995094 at /Users/alex/Downloads/ga-experiments/fetch.py (622ms)
Background analysis message: getSemanticTokens delta
[BG(1)] getSemanticTokens delta previousResultId:1626353164052 at /Users/alex/Downloads/ga-experiments/tokenizer.py ...
[BG(1)]   parsing: /Users/alex/Downloads/ga-experiments/tokenizer.py (6ms)
[BG(1)]   binding: /Users/alex/Downloads/ga-experiments/tokenizer.py (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/__future__.pyi [fs read 0ms] (1ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/__future__.pyi (0ms)
[BG(1)]   parsing: /usr/local/lib/python3.9/site-packages/regex/__init__.py [fs read 0ms] (1ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/regex/__init__.py ...
[BG(1)]     parsing: /usr/local/lib/python3.9/site-packages/regex/regex.py [fs read 14ms] (21ms)
[BG(1)]     binding: /usr/local/lib/python3.9/site-packages/regex/regex.py ...
[BG(1)]       parsing: /usr/local/lib/python3.9/site-packages/regex/_regex_core.py [fs read 9ms] (47ms)
[BG(1)]       binding: /usr/local/lib/python3.9/site-packages/regex/_regex_core.py (34ms)
[BG(1)]     binding: /usr/local/lib/python3.9/site-packages/regex/regex.py (85ms)
[BG(1)]   binding: /usr/local/lib/python3.9/site-packages/regex/__init__.py (106ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/locale.pyi [fs read 1ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/locale.pyi (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/random.pyi [fs read 0ms] (3ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/random.pyi (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/collections/abc.pyi [fs read 0ms] (0ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/collections/abc.pyi (1ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/unicodedata.pyi [fs read 0ms] (1ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/unicodedata.pyi (0ms)
[BG(1)]   parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/textwrap.pyi [fs read 1ms] (2ms)
[BG(1)]   binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/textwrap.pyi (1ms)
[BG(1)] getSemanticTokens delta previousResultId:1626353164052 at /Users/alex/Downloads/ga-experiments/tokenizer.py (273ms)
Background analysis message: analyze
[BG(1)] analyzing: /Users/alex/Downloads/ga-experiments/fetch.py ...
[BG(1)]   checking: /Users/alex/Downloads/ga-experiments/fetch.py (10ms)
[BG(1)] analyzing: /Users/alex/Downloads/ga-experiments/fetch.py (10ms)
[BG(1)] analyzing: /Users/alex/Downloads/ga-experiments/tokenizer.py ...
[BG(1)]   checking: /Users/alex/Downloads/ga-experiments/tokenizer.py (6ms)
[BG(1)] analyzing: /Users/alex/Downloads/ga-experiments/tokenizer.py (6ms)
Background analysis message: resumeAnalysis
Background analysis message: getDiagnosticsForRange
Background analysis message: getDiagnosticsForRange
[FG] parsing: /Users/alex/Downloads/ga-experiments/tokenizer.py (20ms)
[FG] parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/builtins.pyi [fs read 0ms] (41ms)
[FG] binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/builtins.pyi (15ms)
[FG] binding: /Users/alex/Downloads/ga-experiments/tokenizer.py (1ms)
Background analysis message: getDiagnosticsForRange
Background analysis message: getDiagnosticsForRange
Background analysis message: getDiagnosticsForRange
Background analysis message: getDiagnosticsForRange
[FG] parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing.pyi [fs read 8ms] (21ms)
[FG] binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing.pyi (4ms)
[FG] parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/_typeshed/__init__.pyi [fs read 1ms] (5ms)
[FG] binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/_typeshed/__init__.pyi (2ms)
[FG] parsing: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing_extensions.pyi [fs read 0ms] (2ms)
[FG] binding: /Users/alex/.vscode/extensions/ms-python.vscode-pylance-2021.7.2/dist/typeshed-fallback/stdlib/typing_extensions.pyi (0ms)
Background analysis message: getDiagnosticsForRange
Background analysis message: getDiagnosticsForRange
@alextremblay
Copy link
Author

Mind you, i have no idea if what I'm doing is the correct way to annotate metaclass methods that return instances of the metaclass. I don't know of any other way, and can't find documentation anywhere on how to achieve this.

If you know of a better way for me to annotate this, I'd love to hear it!

Thanks

@erictraut
Copy link
Contributor

By default, type checking is disabled in pylance. It appears that you have enabled it — probably by setting "python.analysis.typeCheckingMode" to "basic".

You are dynamically generating a new class here. A static type checker like pyright (upon which pylance is built) must understand the class hierarchy. I recommend against using this form of dynamic class creation if you are interested in static type checking.

If you have no way to avoid dynamic class creation, you can use a # type: ignore comment to suppress this specific error, but you may see other false positive or false negative errors when you try to use these dynamically-created classes.

I'll also note that the code in your sample claims to be generating a class using a "metaclass", but that's not what it's actually doing. Instead, it's creating a new class with TokenMeta as a base class. That's very different from a metaclass. In this example, type is the metaclass that is being instantiated. If you really meant for the class to use cls as a metaclass, you should be instantiating it directly like this: new_subclass = cls(new_subclass_name, (), {}).

@alextremblay
Copy link
Author

alextremblay commented Jul 15, 2021

Hmmm... that's a very comprehensive answer, thank you.

I'm not sure i understand what you're saying regarding class instantiation.

In my example, if i write new_class = Token.SubToken, __getattr__(cls, name) will be called on Token, cls will be Token, and name will be 'SubToken'
new_class will then be a subclass of Token.
If you look at the code in the example in the docstring of my Token class, you'll see what i mean.

what I'm trying to do with the signature def __getattr__(cls: TokenMeta, name: str) -> TokenMeta: is to tell the type checker "this method will be called with cls being an instance of TokenMeta (like, for example Token, and this method will return an instance of TokenMeta"

What I'd really like to do is tell the type checker "cls on this method is a class which is an instance of this metaclass, and the return value of this method will be a subclass of that class", since that's what's actually happening. i just don't know what kind of type hinting black magic is required to tell that to the type checker

@erictraut
Copy link
Contributor

Ah, I misinterpreted the code. Forget what I said above in the last paragraph about the metaclass.

Here's how I would annotate this:

    def __getattr__(cls, name: str) -> "Type[Token]":
        if name[0].isupper():
            parent_class_name = cls.__name__
            new_subclass_name = f"{parent_class_name}.{name}"
            new_subclass = cast(Type[Token], type(new_subclass_name, (cls,), {})) # type: ignore

I think I now understand what this code is trying to do. It falls into the category of "someone was trying to be too clever". I recommend looking for better ways to implement this functionality if that's an option. This approach is difficult to understand and not conducive to static type checking.

@erictraut
Copy link
Contributor

Actually, it's probably not wise for the metaclass to reference a Token in its annotation. It might be best just to do this:

    def __getattr__(cls, name: str) -> type:
        if name[0].isupper():
            parent_class_name = cls.__name__
            new_subclass_name = f"{parent_class_name}.{name}"
            new_subclass = type(new_subclass_name, (cls,), {})  # type: ignore

@alextremblay
Copy link
Author

yeah, my initial implementation specifically referenced Type[Token], but i thought the same thing you did, that it's not good to hardcode a specific instance of the metaclass in that metaclasses' type signature, which is why i tried to be clever and make it more abstract

I think you're right, that this whole endeavour of trying to tell the type checker what I'm doing is a wash. All i really want to do is to tell the type checker that Token.Something is a subclass of Token.

I also tried things like t: Token = Token.Group, by pyright didn't like that either

@alextremblay
Copy link
Author

oh! I think i figured it out:

from typing import Any, Callable, Generic, List, Literal, Tuple, Type, Union, TypeVar

T = TypeVar('T')

class TokenMeta(type, Generic[T]): 
    """Metaclass for dynamic attribute access on class (not on class instance)
    
    With any class you use this metaclass in, references to capital-case attributes 
    on that class will return named subclasses of that class
    
    see Token as an example"""
    
    def __getattr__(cls, name: str) -> Type[T]:
        if name[0].isupper():
            parent_class_name = cls.__name__
            new_subclass_name = f'{parent_class_name}.{name}'
            new_subclass = type(new_subclass_name, (cls,), {}) # type: ignore
            # register this subclass as an attribute on its parent class
            # next time it is referenced, that attribute will be used, and this method will not be called
            setattr(cls, name, new_subclass)

            return new_subclass # type: ignore
        #else:
        raise AttributeError

class Token(str, metaclass=TokenMeta['Token']):
    def __repr__(self,):
        return f'{self.__class__.__name__}({super().__repr__()})'

No, VS Code gives me all the autocompletion goodness i was looking for:
image
image

Thank you so much! you got me thinking in exactly the direction i needed to figure it out. Cheers!

@alextremblay
Copy link
Author

Hi,

sorry, i just realized this issue maybe shouldn't be closed. I have a workaround for my specific problem, but the root issue remains:

I believe the error "Expected class type but received "TokenMeta"" is a bug, since TokenMeta is an instance of a metaclass and is therefore in fact a class type.

Thoughts?

@alextremblay alextremblay reopened this Jul 16, 2021
@erictraut
Copy link
Contributor

A type analyzer doesn't know that a particular class is going to be used as a metaclass when it is analyzing it. Furthermore, the analyzer has no way of knowing the specific type of cls, so it cannot know the class hierarchy of the dynamically-created class, and therefore all type analysis related to that class will be disabled. For those reasons, I think the error needs to stay.

@alextremblay
Copy link
Author

ok, fair enough. Thank you

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

No branches or pull requests

3 participants