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

mypy doesn't seem to know an instance attribute is checked by a decorator #7841

Closed
hamez0r opened this issue Nov 1, 2019 · 1 comment
Closed

Comments

@hamez0r
Copy link

hamez0r commented Nov 1, 2019

Please provide more information to help us understand the issue:
mypy doesn't see an instance attribute is checked by a decorator, in an instance method.

  • Please insert below the code you are checking with mypy,
    or a mock-up repro if the source is private. We would appreciate
    if you try to simplify your case to a minimal repro.
from abc import ABC
from enum import Enum, unique
from functools import wraps
from typing import Optional, Callable


@unique
class Role(Enum):
    USER = 'user'
    ADMIN = 'admin'


class User(object):
    def __init__(self, email: str, password: str, role: Role):
        self.email = email
        self.password = password
        self.role = role


class InitiatedCommand(ABC):
    def __init__(self, initiator: Optional[User]):
        self.initiator = initiator
        self.require_initiator = True


class MissingInitiator(Exception):
    pass


def check_initiator(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(self: InitiatedCommand, *args, **kwargs):
        if not self.initiator:
            if self.require_initiator:
                raise MissingInitiator
            return
        return func(self, *args, **kwargs)
    return wrapper


class NotAllowed(Exception):
    pass


class UpdateUser(InitiatedCommand):
    def __init__(self, user: User, initiator: Optional[User] = None):
        self.user = user
        super().__init__(initiator)

        self.email = None
        self.password = None
        self.role = None

    @check_initiator
    def update_role(self) -> None:
        if self.initiator.role != Role.ADMIN:
            raise NotAllowed

        self.user.role = self.role

    def execute(self):
        self.update_role()


def test_initiator_is_required() -> None:
    user = User(email='[email protected]', password='super-secret', role=Role.USER)

    update_user = UpdateUser(user)
    user.role = Role.ADMIN

    try:
        update_user.execute()
    except MissingInitiator:
        print('Yep, this is expected')


def test_initiator_disabled_on_purpose() -> None:
    user = User(email='[email protected]', password='super-secret', role=Role.USER)

    update_user = UpdateUser(user)
    update_user.require_initiator = False

    user.role = Role.ADMIN

    update_user.execute()
    print('User updated without initiator')
    assert user.role == Role.ADMIN
  • What is the actual behavior/output?
    mypy output
root@8506a4af0701:/app# mypy tests/test_instance_decorator.py
tests/test_instance_decorator.py:56: error: Item "None" of "Optional[User]" has no attribute "role"
tests/test_instance_decorator.py:59: error: Incompatible types in assignment (expression has type "None", variable has type "Role")
Found 2 errors in 1 file (checked 1 source file)

pytest output:

root@8506a4af0701:/app# py.test tests/test_instance_decorator.py -s -vv
==================================================================== test session starts ====================================================================
platform linux -- Python 3.6.8, pytest-4.6.6, py-1.8.0, pluggy-0.13.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /anmo-app
plugins: cov-2.8.1
collected 2 items

tests/test_instance_decorator.py::test_initiator_is_required Yep, this is expected
PASSED
tests/test_instance_decorator.py::test_initiator_disabled_on_purpose User updated without initiator
PASSED
  • What is the behavior/output you expect?
    I would expect mypy realized I'm checking that instance attribute in the decorator.

  • What are the versions of mypy and Python you are using?
    7.4.0

  • What are the mypy flags you are using? (For example --strict-optional)
    None

  • More context about what I'm trying to do
    I'm building a small app that will be wrapped in a REST API. There are Users that have roles (user and admin). The business logic says a user's role can be elevated only by an admin (so that's what's up with the initiator). However, I want to use this same command from CLI, where I can manually disable the initiator check.

@hamez0r
Copy link
Author

hamez0r commented Nov 1, 2019

I just realized I'm looking for a macro-like behavior, and I don't think mypy should tap into that.

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

1 participant