Skip to content

Commit c951ee5

Browse files
Replace ClassProperty with metaclass properties
Python 3.10+ doesn't have a built-in class property decorator (the @classmethod + @Property chaining was deprecated in 3.11). The modern approach is to define properties on the metaclass, which automatically makes them work at the class level. - Move connection, table_name, full_table_name properties to TableMeta - Create PartMeta subclass with overridden properties for Part tables - Remove ClassProperty class from utils.py Co-authored-by: dimitri-yatsenko <[email protected]>
1 parent b4512c9 commit c951ee5

File tree

2 files changed

+43
-49
lines changed

2 files changed

+43
-49
lines changed

src/datajoint/user_tables.py

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .autopopulate import AutoPopulate
88
from .errors import DataJointError
99
from .table import Table
10-
from .utils import ClassProperty, from_camel_case
10+
from .utils import from_camel_case
1111

1212
_base_regexp = r"[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*"
1313

@@ -78,6 +78,26 @@ def __add__(cls, arg):
7878
def __iter__(cls):
7979
return iter(cls())
8080

81+
# Class properties - defined on metaclass to work at class level
82+
@property
83+
def connection(cls):
84+
"""The database connection for this table."""
85+
return cls._connection
86+
87+
@property
88+
def table_name(cls):
89+
"""The table name formatted for MySQL."""
90+
if cls._prefix is None:
91+
raise AttributeError("Class prefix is not defined!")
92+
return cls._prefix + from_camel_case(cls.__name__)
93+
94+
@property
95+
def full_table_name(cls):
96+
"""The fully qualified table name (`database`.`table`)."""
97+
if cls.database is None:
98+
return None
99+
return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name)
100+
81101

82102
class UserTable(Table, metaclass=TableMeta):
83103
"""
@@ -101,27 +121,6 @@ def definition(self):
101121
"""
102122
raise NotImplementedError('Subclasses of Table must implement the property "definition"')
103123

104-
@ClassProperty
105-
def connection(cls):
106-
return cls._connection
107-
108-
@ClassProperty
109-
def table_name(cls):
110-
"""
111-
:return: the table name of the table formatted for mysql.
112-
"""
113-
if cls._prefix is None:
114-
raise AttributeError("Class prefix is not defined!")
115-
return cls._prefix + from_camel_case(cls.__name__)
116-
117-
@ClassProperty
118-
def full_table_name(cls):
119-
if cls not in {Manual, Imported, Lookup, Computed, Part, UserTable}:
120-
# for derived classes only
121-
if cls.database is None:
122-
raise DataJointError("Class %s is not properly declared (schema decorator not applied?)" % cls.__name__)
123-
return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name)
124-
125124

126125
class Manual(UserTable):
127126
"""
@@ -163,7 +162,28 @@ class Computed(UserTable, AutoPopulate):
163162
tier_regexp = r"(?P<computed>" + _prefix + _base_regexp + ")"
164163

165164

166-
class Part(UserTable):
165+
class PartMeta(TableMeta):
166+
"""Metaclass for Part tables with overridden class properties."""
167+
168+
@property
169+
def table_name(cls):
170+
"""The table name for a Part is derived from its master table."""
171+
return None if cls.master is None else cls.master.table_name + "__" + from_camel_case(cls.__name__)
172+
173+
@property
174+
def full_table_name(cls):
175+
"""The fully qualified table name (`database`.`table`)."""
176+
if cls.database is None or cls.table_name is None:
177+
return None
178+
return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name)
179+
180+
@property
181+
def master(cls):
182+
"""The master table for this Part table."""
183+
return cls._master
184+
185+
186+
class Part(UserTable, metaclass=PartMeta):
167187
"""
168188
Inherit from this class if the table's values are details of an entry in another table
169189
and if this table is populated by the other table. For example, the entries inheriting from
@@ -184,24 +204,6 @@ class Part(UserTable):
184204
+ ")"
185205
)
186206

187-
@ClassProperty
188-
def connection(cls):
189-
return cls._connection
190-
191-
@ClassProperty
192-
def full_table_name(cls):
193-
return (
194-
None if cls.database is None or cls.table_name is None else r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name)
195-
)
196-
197-
@ClassProperty
198-
def master(cls):
199-
return cls._master
200-
201-
@ClassProperty
202-
def table_name(cls):
203-
return None if cls.master is None else cls.master.table_name + "__" + from_camel_case(cls.__name__)
204-
205207
def delete(self, force=False):
206208
"""
207209
unless force is True, prohibits direct deletes from parts.

src/datajoint/utils.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@
77
from .errors import DataJointError
88

99

10-
class ClassProperty:
11-
def __init__(self, f):
12-
self.f = f
13-
14-
def __get__(self, obj, owner):
15-
return self.f(owner)
16-
17-
1810
def user_choice(prompt, choices=("yes", "no"), default=None):
1911
"""
2012
Prompts the user for confirmation. The default value, if any, is capitalized.

0 commit comments

Comments
 (0)