Skip to content

Commit e3135ce

Browse files
Add parameter for non-strict cased output that preserves delimiter count
1 parent 52eea5c commit e3135ce

File tree

2 files changed

+79
-11
lines changed

2 files changed

+79
-11
lines changed

betterproto/casing.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,30 +53,65 @@ def safe_snake_case(value: str) -> str:
5353
return value
5454

5555

56-
def snake_case(value: str):
56+
def snake_case(value: str, strict: bool = True):
5757
"""
5858
Join words with an underscore into lowercase and remove symbols.
59+
@param value: value to convert
60+
@param strict: force single underscores
5961
"""
62+
63+
def substitute_word(symbols, word, is_start):
64+
if not word:
65+
return ""
66+
if strict:
67+
delimiter_count = 0 if is_start else 1 # Single underscore if strict.
68+
elif is_start:
69+
delimiter_count = len(symbols)
70+
elif word.isupper() or word.islower():
71+
delimiter_count = max(1, len(symbols)) # Preserve all delimiters if not strict.
72+
else:
73+
delimiter_count = len(symbols) + 1 # Extra underscore for leading capital.
74+
75+
return ("_" * delimiter_count) + word.lower()
76+
6077
snake = re.sub(
61-
f"{SYMBOLS}({WORD_UPPER}|{WORD})", lambda groups: "_" + groups[1].lower(), value
78+
f"(^)?({SYMBOLS})({WORD_UPPER}|{WORD})",
79+
lambda groups: substitute_word(groups[2], groups[3], groups[1] is not None),
80+
value,
6281
)
63-
return snake.strip("_")
82+
return snake
6483

6584

66-
def pascal_case(value: str):
85+
def pascal_case(value: str, strict: bool = True):
6786
"""
6887
Capitalize each word and remove symbols.
88+
@param value: value to convert
89+
@param strict: output only alphanumeric characters
6990
"""
91+
92+
def substitute_word(symbols, word):
93+
if strict:
94+
return word.capitalize() # Remove all delimiters
95+
96+
if word.islower():
97+
delimiter_length = len(symbols[:-1]) # Lose one delimiter
98+
else:
99+
delimiter_length = len(symbols) # Preserve all delimiters
100+
101+
return ("_" * delimiter_length) + word.capitalize()
102+
70103
return re.sub(
71-
f"{SYMBOLS}({WORD_UPPER}|{WORD})", lambda groups: groups[1].capitalize(), value
104+
f"({SYMBOLS})({WORD_UPPER}|{WORD})",
105+
lambda groups: substitute_word(groups[1], groups[2]),
106+
value,
72107
)
73108

74109

75-
def camel_case(value: str):
110+
def camel_case(value: str, strict: bool = True):
76111
"""
77112
Capitalize all words except first and remove symbols.
78113
"""
79-
return lowercase_first(pascal_case(value))
114+
return lowercase_first(pascal_case(value, strict=strict))
80115

81116

82117
def lowercase_first(value: str):

betterproto/tests/test_casing.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
],
3030
)
3131
def test_pascal_case(value, expected):
32-
actual = pascal_case(value)
32+
actual = pascal_case(value, strict=True)
3333
assert actual == expected, f"{value} => {expected} (actual: {actual})"
3434

3535

@@ -56,8 +56,22 @@ def test_pascal_case(value, expected):
5656
("1foobar", "1Foobar"),
5757
],
5858
)
59-
def test_camel_case(value, expected):
60-
actual = camel_case(value)
59+
def test_camel_case_strict(value, expected):
60+
actual = camel_case(value, strict=True)
61+
assert actual == expected, f"{value} => {expected} (actual: {actual})"
62+
63+
64+
@pytest.mark.parametrize(
65+
["value", "expected"],
66+
[
67+
("foo_bar", "fooBar"),
68+
("FooBar", "fooBar"),
69+
("foo__bar", "foo_Bar"),
70+
("foo__Bar", "foo__Bar"),
71+
],
72+
)
73+
def test_camel_case_not_strict(value, expected):
74+
actual = camel_case(value, strict=False)
6175
assert actual == expected, f"{value} => {expected} (actual: {actual})"
6276

6377

@@ -71,6 +85,7 @@ def test_camel_case(value, expected):
7185
("FooBar", "foo_bar"),
7286
("foo.bar", "foo_bar"),
7387
("foo_bar", "foo_bar"),
88+
("foo_Bar", "foo_bar"),
7489
("FOOBAR", "foobar"),
7590
("FOOBar", "foo_bar"),
7691
("UInt32", "u_int32"),
@@ -85,8 +100,26 @@ def test_camel_case(value, expected):
85100
("foo~bar", "foo_bar"),
86101
("foo:bar", "foo_bar"),
87102
("1foobar", "1_foobar"),
103+
("GetUInt64", "get_u_int64"),
88104
],
89105
)
90-
def test_snake_case(value, expected):
106+
def test_snake_case_strict(value, expected):
91107
actual = snake_case(value)
92108
assert actual == expected, f"{value} => {expected} (actual: {actual})"
109+
110+
111+
@pytest.mark.parametrize(
112+
["value", "expected"],
113+
[
114+
("fooBar", "foo_bar"),
115+
("FooBar", "foo_bar"),
116+
("foo_Bar", "foo__bar"),
117+
("foo__bar", "foo__bar"),
118+
("FOOBar", "foo_bar"),
119+
("__foo", "__foo"),
120+
("GetUInt64", "get_u_int64"),
121+
],
122+
)
123+
def test_snake_case_not_strict(value, expected):
124+
actual = snake_case(value, strict=False)
125+
assert actual == expected, f"{value} => {expected} (actual: {actual})"

0 commit comments

Comments
 (0)