Skip to content

Commit

Permalink
feat: handle ClassVar annotations in Corgy classes
Browse files Browse the repository at this point in the history
  • Loading branch information
jayanthkoushik committed Oct 22, 2022
1 parent 01b2d22 commit 6d83ab4
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 2 deletions.
28 changes: 27 additions & 1 deletion corgy/_corgy.py
Expand Up @@ -10,6 +10,7 @@
from typing import (
Any,
Callable,
ClassVar,
Dict,
IO,
Mapping,
Expand Down Expand Up @@ -159,7 +160,7 @@ def __new__(cls, name, bases, namespace, **kwds):
return tempnew

del tempnew # YUCK
for var_name in namespace["__annotations__"]:
for var_name in set(namespace["__annotations__"].keys()):
var_ano = type_hints[var_name]
# Check for name conflicts.
_mangled_name = f"_{name.lstrip('_')}__{var_name}"
Expand Down Expand Up @@ -194,6 +195,16 @@ def __new__(cls, name, bases, namespace, **kwds):
)
else:
var_flags = None
elif hasattr(var_ano, "__origin__") and var_ano.__origin__ is ClassVar:
# `<var_name>: ClassVar[<var_type>]`
# Make sure the class variable has an associated value.
if var_name not in namespace:
if var_name in namespace["__defaults"]:
del namespace["__defaults"][var_name]
else:
raise TypeError(f"class variable `{var_name}` has no value set")
del namespace["__annotations__"][var_name]
continue
else:
# `<var_name>: <var_type>`.
var_type = var_ano
Expand Down Expand Up @@ -320,6 +331,21 @@ class A(Corgy, corgy_make_slots=False):
a.y = 1 # `Corgy` variable
a.x = 2 # custom variable
Names marked with the `ClassVar` type will be added as class variables, and will
not be available as `Corgy` variables::
class A(Corgy):
x: ClassVar[int] = 3
A.x # OK (returns `3`)
A.x = 4 # OK
a = A()
a.x # OK (returns `3`)
a.x = 4 # ERROR!
Also note that class variables need to be assigned to a value during
definition, and this value will not be type checked by `Corgy`.
Inheritance works as expected, whether base classes are themselves `Corgy` classes
or not, with sub-classes inheriting the attributes of the base class, and overriding
any redefined attributes::
Expand Down
17 changes: 17 additions & 0 deletions docs/corgy.md
Expand Up @@ -71,6 +71,23 @@ a.y = 1 # `Corgy` variable
a.x = 2 # custom variable
```

Names marked with the `ClassVar` type will be added as class variables, and will
not be available as `Corgy` variables:

```python
class A(Corgy):
x: ClassVar[int] = 3

A.x # OK (returns `3`)
A.x = 4 # OK
a = A()
a.x # OK (returns `3`)
a.x = 4 # ERROR!
```

Also note that class variables need to be assigned to a value during
definition, and this value will not be type checked by `Corgy`.

Inheritance works as expected, whether base classes are themselves `Corgy` classes
or not, with sub-classes inheriting the attributes of the base class, and overriding
any redefined attributes:
Expand Down
77 changes: 76 additions & 1 deletion tests/test_corgy.py
Expand Up @@ -2,7 +2,7 @@
import sys
import unittest
from io import BytesIO
from typing import Optional, Sequence, Tuple
from typing import ClassVar, Optional, Sequence, Tuple
from unittest import skipIf
from unittest.mock import MagicMock, patch

Expand Down Expand Up @@ -224,6 +224,81 @@ class D2(C2):
self.assertEqual(d.y, 2)
self.assertEqual(d.z, 3)

def test_corgy_cls_does_not_add_classvar(self):
class C(Corgy):
x: ClassVar[int] = 0

self.assertEqual(C.x, 0)
c = C()
self.assertEqual(c.x, 0)
with self.assertRaises(AttributeError):
c.x = 1
C.x = 1
self.assertEqual(C.x, 1)
self.assertEqual(c.x, 1)

def test_corgy_cls_raises_if_classvar_has_no_value(self):
with self.assertRaises(TypeError):

class _(Corgy):
x: ClassVar[int]

def test_corgy_cls_inherits_classvar(self):
class C:
x: ClassVar[int] = 0
y = 1
__slots__ = ()

class D(C, Corgy):
z: ClassVar[str] = "0"

class CCorgy(Corgy):
x: ClassVar[int] = 0
y = 1

class DCorgy(CCorgy):
z: ClassVar[str] = "0"

for cls in (D, DCorgy):
obj = cls()
for _attr, _val in zip(("x", "y", "z"), (0, 1, "0")):
with self.subTest(cls=cls.__name__, attr=_attr):
self.assertEqual(getattr(cls, _attr), _val)
self.assertEqual(getattr(obj, _attr), _val)
with self.assertRaises(AttributeError):
setattr(obj, _attr, "no")

def test_corgy_cls_can_override_inherited_classvar(self):
class C:
x: ClassVar[int] = 0
y = 1
z: ClassVar[str] = "1"

class D(C, Corgy):
x: int # type: ignore
y: str = "1"
z: ClassVar[str] = "2"

class CCorgy(Corgy):
x: ClassVar[int] = 0
y = 1
z: ClassVar[str] = "1"

class DCorgy(CCorgy):
x: int # type: ignore
y: str = "1"
z: ClassVar[str] = "2"

for ccls, dcls in zip((C, CCorgy), (D, DCorgy)):
self.assertEqual(ccls.z, "1")
self.assertEqual(dcls.z, "2")
self.assertIsInstance(dcls.x, property)
self.assertIsInstance(dcls.y, property)
dobj = dcls()
with self.assertRaises(AttributeError):
_ = dobj.x
self.assertEqual(dobj.y, "1")

def test_corgy_cls_has_correct_repr_str(self):
c = self._CorgyCls()
c.x1 = [0, 1]
Expand Down

0 comments on commit 6d83ab4

Please sign in to comment.