Skip to content

Commit fd8034c

Browse files
fix(#1433): allow leading underscore in attribute names
The attribute_name parser in declare.py was pp.Word over [a-z] init chars and [a-z0-9_] body chars, rejecting any name starting with `_`. But the framework already treats names starting with `_` as hidden attributes (Heading.attributes filters by is_hidden = name.startswith("_")), and internal hidden columns like _job_start_time, _job_duration, _job_version, and _singleton are injected programmatically, bypassing the parser. User-defined hidden attributes — documented at docs.datajoint.com/reference/specs/table-declaration/#34-hidden-attributes — hit the parser and failed with a cryptic pyparsing ParseException. Allow `_` in the init-chars set so user code like _params_hash: varchar(32) unique index (tool, _params_hash) declares cleanly. Names starting with a digit are still rejected. Fixes #1433
1 parent dde6471 commit fd8034c

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

src/datajoint/declare.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def build_attribute_parser() -> pp.ParserElement:
147147
"""
148148
quoted = pp.QuotedString('"') ^ pp.QuotedString("'")
149149
colon = pp.Literal(":").suppress()
150-
attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]")).set_results_name("name")
150+
attribute_name = pp.Word(pp.srange("[_a-z]"), pp.srange("[a-z0-9_]")).set_results_name("name")
151151
data_type = (
152152
pp.Combine(pp.Word(pp.alphas) + pp.SkipTo("#", ignore=quoted))
153153
^ pp.QuotedString("<", end_quote_char=">", unquote_results=False)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Unit tests for hidden attribute names (leading underscore) in table declarations.
2+
3+
Regression coverage for issue #1433: the declaration parser previously rejected
4+
attribute names starting with ``_``, even though hidden-attribute semantics
5+
(``is_hidden = name.startswith("_")``) were already implemented in ``Heading``.
6+
"""
7+
8+
import pytest
9+
10+
from datajoint.declare import attribute_parser
11+
12+
13+
@pytest.mark.parametrize(
14+
"line",
15+
[
16+
"_hidden: bool",
17+
"_params_hash: varchar(32)",
18+
"_job_start_time=null: datetime(3)",
19+
"_a: int",
20+
],
21+
)
22+
def test_parser_accepts_leading_underscore(line):
23+
match = attribute_parser.parse_string(line + "#", parse_all=True)
24+
assert match["name"].startswith("_")
25+
26+
27+
def test_parser_still_accepts_plain_names():
28+
match = attribute_parser.parse_string("name: varchar(40)#", parse_all=True)
29+
assert match["name"] == "name"
30+
31+
32+
def test_parser_rejects_digit_start():
33+
"""Numeric leading char remains invalid (preserved behavior)."""
34+
import pyparsing as pp
35+
36+
with pytest.raises(pp.ParseException):
37+
attribute_parser.parse_string("1bad: int#", parse_all=True)
38+
39+
40+
def test_parser_extracts_hidden_name_for_is_hidden_dispatch():
41+
"""The parsed name is what Heading uses to set is_hidden via name.startswith('_')."""
42+
match = attribute_parser.parse_string("_secret: int#", parse_all=True)
43+
name = match["name"]
44+
assert name.startswith("_")

0 commit comments

Comments
 (0)