Skip to content

Commit db42c26

Browse files
Merge pull request #1441 from datajoint/fix/1433-allow-leading-underscore-in-attribute-names
fix(#1433): clear error for leading-underscore attribute names
2 parents 4e4c788 + 46f4ad9 commit db42c26

2 files changed

Lines changed: 59 additions & 0 deletions

File tree

src/datajoint/declare.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,14 @@ def compile_attribute(
855855
DataJointError
856856
If syntax is invalid, primary key is nullable, or blob has invalid default.
857857
"""
858+
if line.lstrip().startswith("_"):
859+
raise DataJointError(
860+
f'Attribute name in line "{line}" starts with an underscore. '
861+
"Names with leading underscore are reserved for platform-managed "
862+
"columns (e.g. _job_start_time, _singleton). Use a regular "
863+
"attribute name; if you need to control visibility at the call "
864+
"site, use proj()."
865+
)
858866
try:
859867
match = attribute_parser.parse_string(line + "#", parse_all=True)
860868
except pp.ParseException as err:
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Unit tests for the leading-underscore guard in attribute declarations.
2+
3+
Regression coverage for issue #1433: declarations like ``_hidden: bool``
4+
previously failed with a cryptic ``pyparsing.ParseException``. The framework
5+
intentionally does not support user-defined hidden attributes — those names
6+
are reserved for platform-managed columns (e.g. ``_job_start_time``,
7+
``_singleton``) which DataJoint injects programmatically after parsing.
8+
9+
This test ensures the user gets a clear ``DataJointError`` pointing to the
10+
right alternative, not a parser-internals error.
11+
"""
12+
13+
import pytest
14+
15+
from datajoint.declare import attribute_parser, compile_attribute
16+
from datajoint.errors import DataJointError
17+
18+
19+
@pytest.mark.parametrize(
20+
"line",
21+
[
22+
"_hidden: bool",
23+
"_params_hash: varchar(32)",
24+
" _leading_whitespace: int32",
25+
],
26+
)
27+
def test_compile_attribute_rejects_leading_underscore(line):
28+
"""The leading-underscore guard fires before the parser, so adapter is unused."""
29+
with pytest.raises(DataJointError, match="reserved for platform-managed"):
30+
compile_attribute(line, in_key=False, foreign_key_sql=[], context={}, adapter=None)
31+
32+
33+
def test_parser_still_rejects_leading_underscore():
34+
"""Parser regex itself remains strict; the helpful error fires before the parser."""
35+
import pyparsing as pp
36+
37+
with pytest.raises(pp.ParseException):
38+
attribute_parser.parse_string("_hidden: bool#", parse_all=True)
39+
40+
41+
def test_parser_still_accepts_plain_names():
42+
match = attribute_parser.parse_string("name: varchar(40)#", parse_all=True)
43+
assert match["name"] == "name"
44+
45+
46+
def test_parser_rejects_digit_start():
47+
"""Numeric leading char remains invalid (preserved behavior)."""
48+
import pyparsing as pp
49+
50+
with pytest.raises(pp.ParseException):
51+
attribute_parser.parse_string("1bad: int32#", parse_all=True)

0 commit comments

Comments
 (0)