Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

bpo-34963: Create callable types in typing.NewType#9808

Closed
fish2000 wants to merge 2 commits into
python:masterpython/cpython:masterfrom
fish2000:patch-2Copy head branch name to clipboard
Closed

bpo-34963: Create callable types in typing.NewType#9808
fish2000 wants to merge 2 commits into
python:masterpython/cpython:masterfrom
fish2000:patch-2Copy head branch name to clipboard

Conversation

@fish2000

@fish2000 fish2000 commented Oct 12, 2018

Copy link
Copy Markdown
Contributor

This allows the generated types to furnish a real __repr__ function, while retaining the same behavior (specifically, the near-zero runtime overhead).

https://bugs.python.org/issue34963

Comment thread Lib/typing.py Outdated
Comment thread Lib/typing.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd argue that it's best you asserted this inside a test in Lib/test/test_typing.py instead of here since this adds checking overhead every time this is used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds like a better deal, indeed. I’ll update the PR. Thanks!

Comment thread Lib/typing.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this should work similar to how namedtuple works, right?

>>> Point = namedtuple('Point', ['x', 'y'])
>>> Point.__name__, Point.__qualname__, Point.__module__
('Point', 'Point', '__main__')
>>> Point.__doc__
'Point(x, y)'
>>> repr(Point)
"<class '__main__.Point'>"
  • for __qualname__ I'm not sure, but seems to be same as __name__, so just simplified?
>>> class A:
...       Point = namedtuple('Point', ['x', 'y'])
... 
>>> A.Point.__qualname__
'Point'
  • for __module__ I see namedtuple used sys._get_frame as others in this file do as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I’m putting __qualname__ in as the same value as __name__ right now… I looked into the __module__ code from namedtuple you linked me to, and that looks like it somewhat violates the whole “almost zero runtime overhead” notion. I am going to implement NewType as a callable class with __slots__ (q.v. notes below) and put the __module__ code with sys._get_frame in a separate PR, so we can bench it out. Yes?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure, I haven't looked into sys._get_frame and how fast that would be. I'm not sure if people usually pickle a type itself, but maybe they do expect it to behave like a normal types/classes which are all pickle-able.

@ilevkivskyi ilevkivskyi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have one comment about the general idea. Could you also please add the benchmark for NewType creation with your fix vs current version?

Comment thread Lib/typing.py Outdated

@ilevkivskyi ilevkivskyi Oct 14, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TBH, I don't really like this. One of the main motivation for NewType was to avoid any overhead of creating new class objects. So this essentially defeats the purpose of NewType. If you really think nice repr is so important, then you can create a special class once in the typing module, and then just instantiate it. For example:

>>> class NewType:
...     def __init__(self, name, tp):
...         self.__name__ = name
...         self.__supertype__ = tp
...     def __call__(self, arg):
...         return arg
...     def __repr__(self):
...         return f"NewType <{self.__name__}:{self.__supertype__.__name__}>"
... 
>>> 
>>> CustomerId = NewType("CustomerId", int)
>>> 
>>> CustomerId
NewType <CustomerId:int>
>>> CustomerId(12)
12

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I’d definitely rather do something like this – presumably with __slots__ defined for the __name__ and __supertype__ values, n’est ce pas? I’ll update the PR accordingly, yes!

@fish2000

Copy link
Copy Markdown
Contributor Author

@ilevkivskyi I would be happy to add some benchmarks – can you do me a huge favor, if you could, and point me at an example of how I should set those up, within the cpython codebase? I have tests running OK but I assume there is a separate suite for performance tuning?… please do pardon my naiveté, erm. Thanks!

@fish2000

fish2000 commented Oct 16, 2018

Copy link
Copy Markdown
Contributor Author

@andreip @gvanrossum @ilevkivskyi The new patch has been pushed up – I squashed the previous volley of commits down into just the one – and it incorporates all of your gracious feedback, as well as unit tests for all of the additions. It looks like Travis and AppVeyor are OK with things, so far. Yes!

N.B. I added a __hash__ method, with the idea that if one needs to cache NewType instances down the road, they can be used as e.g. dict keys, to allow for a simpler cache mechanism than having to use another instance of functools.lru_cache or suchlike.

@andreip

andreip commented Oct 16, 2018

Copy link
Copy Markdown
Contributor

@fish2000 I tried to see if mypy (v0.630) behaves the same way with the new updates and it doesn't. I basically did this experiment:

# newtype.py
import typing

# insert the updated NewType class here

UserId = NewType('UserId', int)  # doesn't work as typing.NewType
def name_by_id(user_id: UserId) -> str:
    ...
UserId('user')          # Fails type check
name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK
num = UserId(5) + 1     # type: int

and then do mypy newtype.py with both the class defined and the one from typing.NewType.

For updated class I get an error like Invalid type "newtype.UserId" while for the typing.NewType I get ok errors like:

newtype.py:44: error: Argument 1 to "UserId" has incompatible type "str"; expected "int"
newtype.py:45: error: Argument 1 to "name_by_id" has incompatible type "int"; expected "UserId"

@fish2000

Copy link
Copy Markdown
Contributor Author

@andreip OK, I will look into that and check it with mypy – perhaps it needs the __module__ attribute – I had been using pyre to check and everything looks exactly the same in both cases (running pyre check with identical inputs and only the typing changes differing).

@fish2000

Copy link
Copy Markdown
Contributor Author

@andreip Yep – it looks like mypy needs to see that the name of the NewType expression is explicitly typing.NewTypethis seems to be the key line of code. Let me verify that it’ll work if it’s in the right place with the right name.

@ilevkivskyi

Copy link
Copy Markdown
Member

@fish2000 mypy totally doesn't care about what happens at runtime. It never runs your code.

@ilevkivskyi

Copy link
Copy Markdown
Member

I would be happy to add some benchmarks – can you do me a huge favor, if you could, and point me at an example of how I should set those up, within the cpython codebase?

I meant just a simple comparison using timeit.timeit().

@ilevkivskyi ilevkivskyi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just two random comments. But we need to decide which approach we will take. Since now there is a "competing" PR #995, I think we can discuss this in the b.p.o. issue.

Comment thread Lib/typing.py
f"{self.__supertype__.__name__}>"

def __hash__(self):
return hash((self.__name__, self.__supertype__))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why this is needed?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have the same question.

Comment thread Lib/typing.py
return new_type
def __repr__(self):
""" NewType reprs are in the form:
“NewTClassName<typename:supertypename>”, e.g.:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please don't use non-ASCII quotes. Also I am not sure this docstirng is actually needed, the code is self-explanatory.

Comment thread Lib/typing.py
new_type.__name__ = name
new_type.__supertype__ = tp
return new_type
def __repr__(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would make the repr in the form NewType("typename", supertype):

    def __repr__(self):
        return f"{type(self).__name__}({self.__name__!r}, {self.__supertype__})"

Comment thread Lib/typing.py
__slots__ = ('__name__', '__qualname__', '__supertype__')

def __init__(self, name, tp):
self.__name__ = self.__qualname__ = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Setting __qualname__ is not needed if it is not a function.

Comment thread Lib/typing.py
f"{self.__supertype__.__name__}>"

def __hash__(self):
return hash((self.__name__, self.__supertype__))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have the same question.

Comment thread Lib/typing.py
by static type checkers. At runtime, NewType(name, tp) returns
a dummy function that simply returns its argument. Usage::
"""NewType creates simple unique types with almost zero runtime
overhead. `NewType(name, tp)` is considered a subtype of `tp`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't use backquotes in docstrings. They are not interpreted.

Comment thread Lib/test/test_typing.py
self.assertEqual(UserName.__qualname__, 'UserName')
self.assertIs(UserName.__supertype__, str)
self.assertIsInstance(UserName, NewType)
self.assertNotEqual(hash(UserId), hash(UserName))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can't guarantee this.

@csabella

Copy link
Copy Markdown
Contributor

@fish2000, please address the code reviews. Thank you!

@vstinner vstinner closed this May 3, 2021
@vstinner vstinner deleted the branch python:master May 3, 2021 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.