Skip to content

Commit 882c217

Browse files
Abel Milashclaude
andcommitted
Use rule-based pluralization for DisplayCollectionName default
Replaces naive label + "s" with _pluralize() covering consonant-y → -ies (Company → Companies, Category → Categories), s/x/z/ch/sh → -es (Class → Classes, Box → Boxes), and the regular -s case. Callers can still override via display_collection_name for irregular plurals. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e0792d6 commit 882c217

4 files changed

Lines changed: 86 additions & 8 deletions

File tree

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,28 @@ def _label(self, text: str) -> Dict[str, Any]:
978978
],
979979
}
980980

981+
@staticmethod
982+
def _pluralize(word: str) -> str:
983+
"""Return a simple English plural for *word*.
984+
985+
Covers the most common patterns without an external dependency:
986+
- consonant + y → -ies (Company → Companies, Category → Categories)
987+
- s / x / z → -es (Class → Classes, Box → Boxes)
988+
- ch / sh → -es (Match → Matches, Dish → Dishes)
989+
- everything else → -s (Product → Products, Account → Accounts)
990+
991+
For irregular plurals (e.g. Person → People) callers should supply
992+
``display_collection_name`` explicitly.
993+
"""
994+
if not word:
995+
return word
996+
suffix = word.lower()
997+
if suffix.endswith("y") and len(suffix) > 1 and suffix[-2] not in "aeiou":
998+
return word[:-1] + "ies"
999+
if suffix.endswith(("ch", "sh", "s", "x", "z")):
1000+
return word + "es"
1001+
return word + "s"
1002+
9811003
def _to_pascal(self, name: str) -> str:
9821004
parts = re.split(r"[^A-Za-z0-9]+", name)
9831005
return "".join(p[:1].upper() + p[1:] for p in parts if p)
@@ -1020,7 +1042,7 @@ def _create_entity(
10201042
"SchemaName": table_schema_name,
10211043
"DisplayName": self._label(display_name),
10221044
"DisplayCollectionName": self._label(
1023-
display_collection_name if display_collection_name is not None else display_name + "s"
1045+
display_collection_name if display_collection_name is not None else self._pluralize(display_name)
10241046
),
10251047
"Description": self._label(description if description is not None else f"Custom entity for {display_name}"),
10261048
"OwnershipType": "UserOwned",
@@ -1656,7 +1678,7 @@ def _create_table(
16561678
:type primary_column_schema_name: ``str`` | ``None``
16571679
:param display_name: Human-readable display name for the table. Defaults to ``table_schema_name``.
16581680
:type display_name: ``str`` | ``None``
1659-
:param display_collection_name: Plural display name shown in navigation. Defaults to ``display_name + "s"``.
1681+
:param display_collection_name: Plural display name shown in navigation. Defaults to a simple English plural of ``display_name``.
16601682
:type display_collection_name: ``str`` | ``None``
16611683
:param description: Description for the table. Defaults to a generated string.
16621684
:type description: ``str`` | ``None``
@@ -2157,7 +2179,7 @@ def _build_create_entity(
21572179
"SchemaName": table,
21582180
"DisplayName": self._label(label),
21592181
"DisplayCollectionName": self._label(
2160-
display_collection_name if display_collection_name is not None else label + "s"
2182+
display_collection_name if display_collection_name is not None else self._pluralize(label)
21612183
),
21622184
"Description": self._label(description if description is not None else f"Custom entity for {label}"),
21632185
"OwnershipType": "UserOwned",

src/PowerPlatform/Dataverse/operations/batch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ def create(
382382
When omitted, defaults to the table schema name.
383383
:type display_name: str or None
384384
:param display_collection_name: Plural display name shown in navigation (e.g. ``"Products"``).
385-
When omitted, defaults to ``display_name + "s"``.
385+
When omitted, a simple English plural of ``display_name`` is used.
386386
:type display_collection_name: str or None
387387
:param description: Description for the table. When omitted, a default description is generated.
388388
:type description: str or None

src/PowerPlatform/Dataverse/operations/tables.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ def create(
103103
(e.g. ``"Product"``). When omitted, defaults to the table schema name.
104104
:type display_name: :class:`str` or None
105105
:param display_collection_name: Plural display name shown in navigation
106-
(e.g. ``"Products"``). When omitted, defaults to ``display_name + "s"``.
106+
(e.g. ``"Products"``). When omitted, a simple English plural of
107+
``display_name`` is used (e.g. ``"Company"`` → ``"Companies"``).
107108
:type display_collection_name: :class:`str` or None
108109
:param description: Description for the table. When omitted, a default
109110
description is generated.

tests/unit/data/test_odata_internal.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,61 @@ def test_to_pascal_basic(self):
596596
self.assertEqual(client._to_pascal("single"), "Single")
597597

598598

599+
class TestPluralize(unittest.TestCase):
600+
"""Unit tests for _ODataClient._pluralize."""
601+
602+
def _p(self, word: str) -> str:
603+
return _ODataClient._pluralize(word)
604+
605+
# --- regular -s words ---
606+
def test_regular_word(self):
607+
self.assertEqual(self._p("Product"), "Products")
608+
609+
def test_regular_word_account(self):
610+
self.assertEqual(self._p("Account"), "Accounts")
611+
612+
# --- consonant + y → -ies ---
613+
def test_consonant_y_company(self):
614+
self.assertEqual(self._p("Company"), "Companies")
615+
616+
def test_consonant_y_category(self):
617+
self.assertEqual(self._p("Category"), "Categories")
618+
619+
def test_consonant_y_policy(self):
620+
self.assertEqual(self._p("Policy"), "Policies")
621+
622+
# --- vowel + y → -ys (not -ies) ---
623+
def test_vowel_y_key(self):
624+
self.assertEqual(self._p("Key"), "Keys")
625+
626+
def test_vowel_y_day(self):
627+
self.assertEqual(self._p("Day"), "Days")
628+
629+
# --- -s / -x / -z / -ch / -sh → -es ---
630+
def test_s_ending(self):
631+
self.assertEqual(self._p("Class"), "Classes")
632+
633+
def test_x_ending(self):
634+
self.assertEqual(self._p("Box"), "Boxes")
635+
636+
def test_ch_ending(self):
637+
self.assertEqual(self._p("Match"), "Matches")
638+
639+
def test_sh_ending(self):
640+
self.assertEqual(self._p("Dish"), "Dishes")
641+
642+
# --- multi-word display names ---
643+
def test_multi_word(self):
644+
self.assertEqual(self._p("Health Inspection"), "Health Inspections")
645+
646+
def test_multi_word_y(self):
647+
self.assertEqual(self._p("Budget Category"), "Budget Categories")
648+
649+
# --- edge cases ---
650+
def test_empty_string(self):
651+
self.assertEqual(self._p(""), "")
652+
653+
599654
class TestRequestErrorParsing(unittest.TestCase):
600655
"""Unit tests for _ODataClient._request error response handling."""
601656

@@ -2908,7 +2963,7 @@ def test_display_name_defaults_to_schema_name(self):
29082963
self.assertEqual(body["DisplayName"]["LocalizedLabels"][0]["Label"], "new_TestTable")
29092964

29102965
def test_display_collection_name_derived_from_display_name(self):
2911-
"""_build_create_entity appends 's' to display_name for DisplayCollectionName."""
2966+
"""_build_create_entity uses _pluralize(display_name) for DisplayCollectionName."""
29122967
body = self._body(display_name="Test Table")
29132968
self.assertEqual(body["DisplayCollectionName"]["LocalizedLabels"][0]["Label"], "Test Tables")
29142969

@@ -3020,8 +3075,8 @@ def test_display_collection_name_explicit(self):
30203075
body = json.loads(self.od._build_create_entity("new_TestTable", {}, display_collection_name="Test Tables").body)
30213076
self.assertEqual(body["DisplayCollectionName"]["LocalizedLabels"][0]["Label"], "Test Tables")
30223077

3023-
def test_display_collection_name_defaults_to_display_name_plus_s(self):
3024-
"""_build_create_entity appends 's' to the label when display_collection_name is omitted."""
3078+
def test_display_collection_name_defaults_to_pluralized_label(self):
3079+
"""_build_create_entity uses _pluralize(label) when display_collection_name is omitted."""
30253080
body = self._body(display_name="Widget")
30263081
self.assertEqual(body["DisplayCollectionName"]["LocalizedLabels"][0]["Label"], "Widgets")
30273082

0 commit comments

Comments
 (0)