Skip to content

Commit

Permalink
Add abstract function definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
wesselb committed Jul 11, 2021
1 parent 0911547 commit cfc2403
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 32 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Everybody likes multiple dispatch, just like everybody likes plums.
- [Conversion](#conversion)
- [Promotion](#promotion)
* [Advanced Features](#advanced-features)
- [Abstract Function Definitions](#abstract-function-definitions)
- [Method Precedence](#method-precedence)
- [Parametric Classes](#parametric-classes)
- [Hooking Into Type Inference](#hooking-into-type-inference)
Expand Down Expand Up @@ -769,6 +770,29 @@ TypeError: Cannot convert a "builtins.int" to a "builtins.float".

## Advanced Features

### Abstract Function Definitions

A function can be abstractly defined using `dispatch.abstract`.
When a function is abstractly defined, the function is created, but no methods
are defined.

```python
from plum import dispatch

@dispatch.abstract
def f(x):
pass
```

```python
>>> f
<function <function f at 0x7f9f6820aea0> with 0 method(s)>

>>> @dispatch
... def f(x: int):
... pass
```

### Method Precedence

The keyword argument `precedence` can be set to an integer value to specify
Expand Down
63 changes: 39 additions & 24 deletions plum/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self):
self._functions = {}
self._classes = {}

def __call__(self, f=None, precedence=0):
def __call__(self, method=None, precedence=0):
"""Decorator for a particular signature.
Args:
Expand All @@ -27,29 +27,29 @@ def __call__(self, f=None, precedence=0):
function: Decorator.
"""

# If `f` is not given, some keywords are set: return another decorator.
if f is None:
# If `method` is not given, some keywords are set: return another decorator.
if method is None:

def decorator(f_):
return self(f_, precedence=precedence)

return decorator

signature, return_type = extract_signature(f)
signature, return_type = extract_signature(method)

def construct_function(owner):
return self._add_method(
f,
method,
[signature],
precedence=precedence,
return_type=return_type,
owner=owner,
)

# Defer the construction if `f` is in a class. We defer the construction to
# Defer the construction if `method` is in a class. We defer the construction to
# allow the function to hold a reference to the class.
if is_in_class(f):
return ClassFunction(get_class(f), construct_function)
if is_in_class(method):
return ClassFunction(get_class(method), construct_function)
else:
return construct_function(None)

Expand All @@ -71,30 +71,44 @@ def multi(
"""
signatures = [Sig(*types) for types in signatures]

def decorator(f):
def decorator(method):
def construct_function(owner):
return self._add_method(
f,
method,
signatures,
precedence=precedence,
return_type=return_type,
owner=owner,
)

# Defer the construction if `f` is in a class. We defer the construction to
# allow the function to hold a reference to the class.
if is_in_class(f):
return ClassFunction(get_class(f), construct_function)
# Defer the construction if `method` is in a class. We defer the
# construction to allow the function to hold a reference to the class.
if is_in_class(method):
return ClassFunction(get_class(method), construct_function)
else:
return construct_function(None)

return decorator

def _add_method(self, f, signatures, precedence, return_type, owner):
name = f.__name__
def abstract(self, method):
"""Decorator for an abstract function definition. The abstract function
definition does not implement any methods."""

# If a class is the owner, use a namespace specific for that class.
# Otherwise, use the global namespace.
def construct_abstract_function(owner):
return self._get_function(method, owner)

# Defer the construction if `method` is in a class. We defer the construction to
# allow the function to hold a reference to the class.
if is_in_class(method):
return ClassFunction(get_class(method), construct_abstract_function)
else:
return construct_abstract_function(None)

def _get_function(self, method, owner):
name = method.__name__

# If a class is the owner, use a namespace specific for that class. Otherwise,
# use the global namespace.
if owner:
if owner not in self._classes:
self._classes[owner] = {}
Expand All @@ -104,15 +118,16 @@ def _add_method(self, f, signatures, precedence, return_type, owner):

# Create a new function only if the function does not already exist.
if name not in namespace:
namespace[name] = Function(f, owner=owner)
namespace[name] = Function(method, owner=owner)

# Register the new method.
for signature in signatures:
namespace[name].register(signature, f, precedence, return_type)

# Return the function.
return namespace[name]

def _add_method(self, method, signatures, precedence, return_type, owner):
f = self._get_function(method, owner)
for signature in signatures:
f.register(signature, method, precedence, return_type)
return f

def clear_cache(self):
"""Clear cache."""
for f in self._functions.values():
Expand Down
5 changes: 3 additions & 2 deletions plum/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,9 @@ def __init__(self, f, owner=None):

# Copy metadata.
self.__name__ = f.__name__
self.__doc__ = f.__doc__
self.__qualname__ = f.__qualname__
self.__module__ = f.__module__
self.__doc__ = f.__doc__

@property
def methods(self):
Expand Down Expand Up @@ -501,7 +502,7 @@ def __get__(self, instance, owner):
def __repr__(self):
return (
f"<function {self._f} with "
f"{len(self._pending) + len(self._resolved)} method(s)>"
f"{len(self._pending) + len(self._methods)} method(s)>"
)


Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = plum-dispatch
version = 1.3.1
version = 1.3.2
author = Wessel Bruinsma
author_email = wessel.p.bruinsma@gmail.com
description = Multiple dispatch in Python
Expand Down
96 changes: 91 additions & 5 deletions tests/dispatcher/test_dispatcher.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from typing import Union, List

import pytest

from plum import Dispatcher, NotFoundLookupError


Expand Down Expand Up @@ -93,6 +94,52 @@ def f(x: int):
assert f(1) == "second"


def test_abstract():
dispatch = Dispatcher()

@dispatch.abstract
def f(x):
"""docstring"""

assert len(f.methods) == 0
assert len(f.precedences) == 0
assert f.__doc__ == "docstring"

@dispatch
def f(x: int):
pass

assert len(f.methods) == 1
assert len(f.precedences) == 1
assert f.__doc__ == "docstring"


def test_abstract_in_class():
dispatch = Dispatcher()

class A:
@dispatch.abstract
def f(self, x):
"""docstring"""

assert len(A.f.methods) == 0
assert len(A.f.precedences) == 0
assert A.f.__doc__ == "docstring"

class A:
@dispatch.abstract
def f(self, x):
"""docstring"""

@dispatch
def f(self, x: int):
pass

assert len(A.f.methods) == 1
assert len(A.f.precedences) == 1
assert A.f.__doc__ == "docstring"


def test_metadata_and_printing():
dispatch = Dispatcher()

Expand All @@ -108,30 +155,69 @@ def f():
"""docstring of f"""

assert f.__name__ == "f"
assert f.__doc__ == "docstring of f"
assert f.__qualname__ == "test_metadata_and_printing.<locals>.f"
assert f.__module__ == "tests.dispatcher.test_dispatcher"
assert f.__doc__ == "docstring of f"
assert repr(f) == f"<function {f._f} with 1 method(s)>"

assert f.invoke().__name__ == "f"
assert f.invoke().__doc__ == "docstring of f"
assert f.invoke().__qualname__ == "test_metadata_and_printing.<locals>.f"
assert f.invoke().__module__ == "tests.dispatcher.test_dispatcher"
assert f.invoke().__doc__ == "docstring of f"
n = len(hex(id(f))) + 1 # Do not check memory address and extra ">".
assert repr(f.invoke())[:-n] == repr(f._f)[:-n]

a = A()
g = a.g

assert g.__name__ == "g"
assert g.__doc__ == "docstring of g"
assert g.__qualname__ == "test_metadata_and_printing.<locals>.A.g"
assert g.__module__ == "tests.dispatcher.test_dispatcher"
assert g.__doc__ == "docstring of g"
assert repr(g) == f'<function {A._dispatch._classes[A]["g"]._f} with 1 method(s)>'

assert g.invoke().__name__ == "g"
assert g.invoke().__doc__ == "docstring of g"
assert g.invoke().__qualname__ == "test_metadata_and_printing.<locals>.A.g"
assert g.invoke().__module__ == "tests.dispatcher.test_dispatcher"
assert g.invoke().__doc__ == "docstring of g"
assert repr(g.invoke())[:-n] == repr(A._dispatch._classes[A]["g"]._f)[:-n]


def test_counting():
dispatch = Dispatcher()

@dispatch.abstract
def f(x):
pass

assert repr(f) == f"<function {f._f} with 0 method(s)>"

@dispatch
def f(x: int):
pass

@dispatch
def f(x: int):
pass

# At this point, two methods are pending but not yet resolved. The second is a
# redefinition of the first, but this will only be clear after the methods are
# resolved.
assert repr(f) == f"<function {f._f} with 2 method(s)>"

# Resolve the methods.
f(1)

# Counting should now be right.
assert repr(f) == f"<function {f._f} with 1 method(s)>"

@dispatch
def f(x: str):
pass

assert repr(f) == f"<function {f._f} with 2 method(s)>"


def test_multi():
dispatch = Dispatcher()

Expand Down

0 comments on commit cfc2403

Please sign in to comment.