Skip to content

Commit b8201b3

Browse files
authored
Allow artificial abstractness to take precedence over true abstractness for deftype* and reify* (#945)
Fixes #942
1 parent cc1c6fc commit b8201b3

File tree

7 files changed

+385
-47
lines changed

7 files changed

+385
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
### Changed
1212
* Improved on the nREPL server exception messages by matching that of the REPL user friendly format (#968)
13+
* Types created via `deftype` and `reify` may declare supertypes as abstract (taking precedence over true `abc.ABC` types) and specify their member list using `^:abstract-members` metadata (#942)
1314

1415
### Removed
1516
* Removed `python-dateutil` and `readerwriterlock` as dependencies, switching to standard library components instead (#976)

docs/concepts.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,7 @@ Types may also optionally implement 0 or more Python `"dunder" methods <https://
12431243
The best approximation is :external:py:class:`abc.ABC`, although this type is merely advisory and many libraries and applications eschew its use.
12441244

12451245
For the cases where a host type is not defined as an ``abc.ABC`` instance, users can override the compiler check by setting the ``^:abstract`` meta key on the interface type symbol passed to the ``deftype`` form.
1246+
This is called "artificial abstractness" and it takes precedence over true abstract base classes via ``abc.ABC``.
12461247
For example, take :external:py:class:`argparse.Action` which is required to be implemented for customizing :external:py:mod:`argparse` actions, but which is not defined as an ``abc.ABC``:
12471248

12481249
.. code-block::
@@ -1255,6 +1256,23 @@ Types may also optionally implement 0 or more Python `"dunder" methods <https://
12551256
;; ...
12561257
))
12571258
1259+
Python libraries may also include implicit (documentation-only) abstract methods on their ``abc.ABC`` types.
1260+
Thus, it is sometimes necessary to annotate a base class using ``^{:abstract-members #{...}}`` to designate any methods which should be considered abstract in the base class since the compiler cannot check them for you.
1261+
Take for example :external:py:class:`io.IOBase` which does not declare ``read`` or ``write`` (or in fact *any* members at all in its ``__abstractmethods__`` set!) as part of the interface, but the documentation specifies that they should be considered part of the interface.
1262+
Below, we tell the compiler it is artificially abstract and that ``read`` is a member.
1263+
1264+
.. code-block::
1265+
1266+
(import io)
1267+
1268+
(reify
1269+
^:abstract
1270+
^{:abstract-members #{:read}}
1271+
io/TextIOBase
1272+
(read [n]
1273+
;; ...
1274+
))
1275+
12581276
.. warning::
12591277

12601278
The Basilisp compiler is not currently able to verify that the signature of implemented methods matches the interface or superclass method signature.

src/basilisp/lang/compiler/analyzer.py

Lines changed: 135 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import functools
66
import inspect
77
import logging
8+
import platform
89
import re
910
import sys
1011
import uuid
@@ -27,6 +28,7 @@
2728
Pattern,
2829
Set,
2930
Tuple,
31+
Type,
3032
TypeVar,
3133
Union,
3234
cast,
@@ -56,6 +58,7 @@
5658
NAME_KW,
5759
NS_KW,
5860
REST_KW,
61+
SYM_ABSTRACT_MEMBERS_META_KEY,
5962
SYM_ABSTRACT_META_KEY,
6063
SYM_ASYNC_META_KEY,
6164
SYM_CLASSMETHOD_META_KEY,
@@ -136,7 +139,7 @@
136139
Yield,
137140
deftype_or_reify_python_member_names,
138141
)
139-
from basilisp.lang.interfaces import IMeta, IRecord, ISeq, IType, IWithMeta
142+
from basilisp.lang.interfaces import IMeta, INamed, IRecord, ISeq, IType, IWithMeta
140143
from basilisp.lang.runtime import Var
141144
from basilisp.lang.typing import CompilerOpts, LispForm, ReaderForm
142145
from basilisp.lang.util import OBJECT_DUNDER_METHODS, count, genname, is_abstract, munge
@@ -662,6 +665,7 @@ def get_meta_prop(o: Union[IMeta, Var]) -> Any:
662665

663666

664667
_is_artificially_abstract = _bool_meta_getter(SYM_ABSTRACT_META_KEY)
668+
_artificially_abstract_members = _meta_getter(SYM_ABSTRACT_MEMBERS_META_KEY)
665669
_is_async = _bool_meta_getter(SYM_ASYNC_META_KEY)
666670
_is_mutable = _bool_meta_getter(SYM_MUTABLE_META_KEY)
667671
_is_py_classmethod = _bool_meta_getter(SYM_CLASSMETHOD_META_KEY)
@@ -1666,31 +1670,91 @@ def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-loca
16661670

16671671
def __is_deftype_member(mem) -> bool:
16681672
"""Return True if `mem` names a valid `deftype*` member."""
1669-
return (
1670-
inspect.isfunction(mem)
1671-
or isinstance(mem, (property, staticmethod))
1672-
or inspect.ismethod(mem)
1673-
)
1673+
return inspect.isroutine(mem) or isinstance(mem, (property, staticmethod))
16741674

16751675

16761676
def __is_reify_member(mem) -> bool:
16771677
"""Return True if `mem` names a valid `reify*` member."""
1678-
return inspect.isfunction(mem) or isinstance(mem, property)
1678+
return inspect.isroutine(mem) or isinstance(mem, property)
1679+
1680+
1681+
if platform.python_implementation() == "CPython":
1682+
1683+
def __is_type_weakref(tp: Type) -> bool:
1684+
return getattr(tp, "__weakrefoffset__", 0) > 0
1685+
1686+
else:
1687+
1688+
def __is_type_weakref(tp: Type) -> bool: # pylint: disable=unused-argument
1689+
return True
1690+
1691+
1692+
def __get_artificially_abstract_members(
1693+
ctx: AnalyzerContext, special_form: sym.Symbol, interface: DefTypeBase
1694+
) -> Set[str]:
1695+
if (
1696+
declared_abstract_members := _artificially_abstract_members(
1697+
cast(IMeta, interface.form)
1698+
)
1699+
) is None:
1700+
return set()
1701+
1702+
if (
1703+
not isinstance(declared_abstract_members, lset.PersistentSet)
1704+
or len(declared_abstract_members) == 0
1705+
):
1706+
raise ctx.AnalyzerException(
1707+
f"{special_form} artificially abstract members must be a set of keywords",
1708+
form=interface.form,
1709+
)
16791710

1711+
members = set()
1712+
for mem in declared_abstract_members:
1713+
if isinstance(mem, INamed):
1714+
if mem.ns is not None:
1715+
logger.warning(
1716+
"Unexpected namespace for artificially abstract member to "
1717+
f"{special_form}: {mem}"
1718+
)
1719+
members.add(mem.name)
1720+
elif isinstance(mem, str):
1721+
members.add(mem)
1722+
else:
1723+
raise ctx.AnalyzerException(
1724+
f"{special_form} artificially abstract member names must be one of: "
1725+
f"str, keyword, or symbol; got {type(mem)}",
1726+
form=interface.form,
1727+
)
1728+
return members
16801729

1681-
def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-locals
1730+
1731+
@attr.define
1732+
class _TypeAbstractness:
1733+
"""
1734+
:ivar is_statically_verified_as_abstract: a boolean value which, if True,
1735+
indicates that all bases have been statically verified abstract; if False,
1736+
indicates at least one base could not be statically verified
1737+
:ivar artificially_abstract_supertypes: the set of all super-types which have
1738+
been marked as artificially abstract
1739+
:ivar supertype_already_weakref: if True, a supertype is already marked as
1740+
weakref and therefore the resulting type cannot add "__weakref__" to the
1741+
slots list to enable weakref support
1742+
"""
1743+
1744+
is_statically_verified_as_abstract: bool
1745+
artificially_abstract_supertypes: lset.PersistentSet[DefTypeBase]
1746+
supertype_already_weakref: bool
1747+
1748+
1749+
def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-locals,too-many-statements
16821750
ctx: AnalyzerContext,
16831751
special_form: sym.Symbol,
16841752
fields: Iterable[str],
16851753
interfaces: Iterable[DefTypeBase],
16861754
members: Iterable[DefTypeMember],
1687-
) -> Tuple[bool, lset.PersistentSet[DefTypeBase]]:
1688-
"""Return a tuple of two items indicating the abstractness of the `deftype*` or
1689-
`reify*` super-types. The first element is a boolean value which, if True,
1690-
indicates that all bases have been statically verified abstract. If False, that
1691-
value indicates at least one base could not be statically verified. The second
1692-
element is the set of all super-types which have been marked as artificially
1693-
abstract.
1755+
) -> _TypeAbstractness:
1756+
"""Return an object indicating the abstractness of the `deftype*` or `reify*`
1757+
super-types.
16941758
16951759
In certain cases, such as in macro definitions and potentially inside of
16961760
functions, the compiler will be unable to resolve the named super-type as an
@@ -1708,6 +1772,7 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-loca
17081772
For normal compile-time errors, an `AnalyzerException` will be raised."""
17091773
assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
17101774

1775+
supertype_possibly_weakref = []
17111776
unverifiably_abstract = set()
17121777
artificially_abstract: Set[DefTypeBase] = set()
17131778
artificially_abstract_base_members: Set[str] = set()
@@ -1752,7 +1817,35 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-loca
17521817
if interface_type is object:
17531818
continue
17541819

1755-
if is_abstract(interface_type):
1820+
if isinstance(interface.form, IMeta) and _is_artificially_abstract(
1821+
interface.form
1822+
):
1823+
# Given that artificially abstract bases aren't real `abc.ABC`s and do
1824+
# not annotate their `abstractmethod`s, we can't assert right now that
1825+
# any the type will satisfy the artificially abstract base. However,
1826+
# we can collect any defined methods into a set for artificial bases
1827+
# and assert that any extra methods are included in that set below.
1828+
artificially_abstract.add(interface)
1829+
artificially_abstract_base_members.update(
1830+
map(
1831+
lambda v: v[0],
1832+
inspect.getmembers(interface_type, predicate=is_member),
1833+
)
1834+
)
1835+
# The meta key :abstract-members will give users the escape hatch to force
1836+
# the compiler to recognize those members as abstract members.
1837+
#
1838+
# When dealing with artificially abstract members, it may be the case
1839+
# that members of superclasses of a specific type don't actually declare
1840+
# abstract member methods expected by subclasses. One particular (major)
1841+
# instance is that of `io.IOBase` which does not declare "read" or "write"
1842+
# as abstract members but whose documentation declares both methods should
1843+
# be considered part of the interface.
1844+
artificially_abstract_base_members.update(
1845+
__get_artificially_abstract_members(ctx, special_form, interface)
1846+
)
1847+
supertype_possibly_weakref.append(__is_type_weakref(interface_type))
1848+
elif is_abstract(interface_type):
17561849
interface_names: FrozenSet[str] = interface_type.__abstractmethods__
17571850
interface_property_names: FrozenSet[str] = frozenset(
17581851
method
@@ -1778,21 +1871,7 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-loca
17781871
)
17791872

17801873
all_interface_methods.update(interface_names)
1781-
elif isinstance(interface.form, IMeta) and _is_artificially_abstract(
1782-
interface.form
1783-
):
1784-
# Given that artificially abstract bases aren't real `abc.ABC`s and do
1785-
# not annotate their `abstractmethod`s, we can't assert right now that
1786-
# any the type will satisfy the artificially abstract base. However,
1787-
# we can collect any defined methods into a set for artificial bases
1788-
# and assert that any extra methods are included in that set below.
1789-
artificially_abstract.add(interface)
1790-
artificially_abstract_base_members.update(
1791-
map(
1792-
lambda v: v[0],
1793-
inspect.getmembers(interface_type, predicate=is_member),
1794-
)
1795-
)
1874+
supertype_possibly_weakref.append(__is_type_weakref(interface_type))
17961875
else:
17971876
raise ctx.AnalyzerException(
17981877
f"{special_form} interface must be Python abstract class or object",
@@ -1802,18 +1881,32 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-loca
18021881

18031882
# We cannot compute if there are extra methods defined if there are any
18041883
# unverifiably abstract bases, so we just skip this check.
1805-
if not unverifiably_abstract:
1884+
if unverifiably_abstract:
1885+
logger.warning(
1886+
f"Unable to verify abstractness for {special_form} supertype(s): "
1887+
f"{', '.join(str(e.var) for e in unverifiably_abstract)}"
1888+
)
1889+
else:
18061890
extra_methods = member_names - all_interface_methods - OBJECT_DUNDER_METHODS
18071891
if extra_methods and not extra_methods.issubset(
18081892
artificially_abstract_base_members
18091893
):
1894+
expected_methods = (
1895+
all_interface_methods | artificially_abstract_base_members
1896+
) - OBJECT_DUNDER_METHODS
1897+
expected_method_str = ", ".join(expected_methods)
18101898
extra_method_str = ", ".join(extra_methods)
18111899
raise ctx.AnalyzerException(
18121900
f"{special_form} definition for interface includes members not "
1813-
f"part of defined interfaces: {extra_method_str}"
1901+
f"part of defined interfaces: {extra_method_str}; expected one of: "
1902+
f"{expected_method_str}"
18141903
)
18151904

1816-
return not unverifiably_abstract, lset.set(artificially_abstract)
1905+
return _TypeAbstractness(
1906+
is_statically_verified_as_abstract=not unverifiably_abstract,
1907+
artificially_abstract_supertypes=lset.set(artificially_abstract),
1908+
supertype_already_weakref=any(supertype_possibly_weakref),
1909+
)
18171910

18181911

18191912
__DEFTYPE_DEFAULT_SENTINEL = object()
@@ -1900,10 +1993,7 @@ def _deftype_ast( # pylint: disable=too-many-locals
19001993
interfaces, members = __deftype_or_reify_impls(
19011994
runtime.nthrest(form, 3), ctx, SpecialForm.DEFTYPE
19021995
)
1903-
(
1904-
verified_abstract,
1905-
artificially_abstract,
1906-
) = __deftype_and_reify_impls_are_all_abstract(
1996+
type_abstractness = __deftype_and_reify_impls_are_all_abstract(
19071997
ctx, SpecialForm.DEFTYPE, map(lambda f: f.name, fields), interfaces, members
19081998
)
19091999
return DefType(
@@ -1912,9 +2002,10 @@ def _deftype_ast( # pylint: disable=too-many-locals
19122002
interfaces=vec.vector(interfaces),
19132003
fields=vec.vector(param_nodes),
19142004
members=vec.vector(members),
1915-
verified_abstract=verified_abstract,
1916-
artificially_abstract=artificially_abstract,
2005+
verified_abstract=type_abstractness.is_statically_verified_as_abstract,
2006+
artificially_abstract=type_abstractness.artificially_abstract_supertypes,
19172007
is_frozen=is_frozen,
2008+
use_weakref_slot=not type_abstractness.supertype_already_weakref,
19182009
env=ctx.get_node_env(pos=ctx.syntax_position),
19192010
)
19202011

@@ -2946,18 +3037,16 @@ def _reify_ast(form: ISeq, ctx: AnalyzerContext) -> Reify:
29463037
interfaces, members = __deftype_or_reify_impls(
29473038
runtime.nthrest(form, 1), ctx, SpecialForm.REIFY
29483039
)
2949-
(
2950-
verified_abstract,
2951-
artificially_abstract,
2952-
) = __deftype_and_reify_impls_are_all_abstract(
3040+
type_abstractness = __deftype_and_reify_impls_are_all_abstract(
29533041
ctx, SpecialForm.REIFY, (), interfaces, members
29543042
)
29553043
return Reify(
29563044
form=form,
29573045
interfaces=vec.vector(interfaces),
29583046
members=vec.vector(members),
2959-
verified_abstract=verified_abstract,
2960-
artificially_abstract=artificially_abstract,
3047+
verified_abstract=type_abstractness.is_statically_verified_as_abstract,
3048+
artificially_abstract=type_abstractness.artificially_abstract_supertypes,
3049+
use_weakref_slot=not type_abstractness.supertype_already_weakref,
29613050
env=ctx.get_node_env(pos=ctx.syntax_position),
29623051
)
29633052

src/basilisp/lang/compiler/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class SpecialForm:
3333
DEFAULT_COMPILER_FILE_PATH = "NO_SOURCE_PATH"
3434

3535
SYM_ABSTRACT_META_KEY = kw.keyword("abstract")
36+
SYM_ABSTRACT_MEMBERS_META_KEY = kw.keyword("abstract-members")
3637
SYM_ASYNC_META_KEY = kw.keyword("async")
3738
SYM_KWARGS_META_KEY = kw.keyword("kwargs")
3839
SYM_PRIVATE_META_KEY = kw.keyword("private")

src/basilisp/lang/compiler/generator.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ def _class_ast( # pylint: disable=too-many-arguments
433433
artificially_abstract_bases: Iterable[ast.expr] = (),
434434
is_frozen: bool = True,
435435
use_slots: bool = True,
436+
use_weakref_slot: bool = True,
436437
) -> ast.ClassDef:
437438
"""Return a Python class definition for `deftype` and `reify` special forms."""
438439
return ast_ClassDef(
@@ -489,6 +490,15 @@ def _class_ast( # pylint: disable=too-many-arguments
489490
keywords=[
490491
ast.keyword(arg="eq", value=ast.Constant(False)),
491492
ast.keyword(arg="slots", value=ast.Constant(use_slots)),
493+
*(
494+
[]
495+
if use_weakref_slot
496+
else [
497+
ast.keyword(
498+
arg="weakref_slot", value=ast.Constant(False)
499+
)
500+
]
501+
),
492502
],
493503
),
494504
],
@@ -1489,6 +1499,7 @@ def _deftype_to_py_ast( # pylint: disable=too-many-locals
14891499
artificially_abstract_bases=artificially_abstract_bases,
14901500
is_frozen=node.is_frozen,
14911501
use_slots=True,
1502+
use_weakref_slot=node.use_weakref_slot,
14921503
),
14931504
ast.Call(
14941505
func=_INTERN_VAR_FN_NAME,
@@ -2708,6 +2719,7 @@ def _reify_to_py_ast(
27082719
artificially_abstract_bases=artificially_abstract_bases,
27092720
is_frozen=True,
27102721
use_slots=True,
2722+
use_weakref_slot=node.use_weakref_slot,
27112723
)
27122724
],
27132725
)

0 commit comments

Comments
 (0)