Skip to content

Commit 74b1ed2

Browse files
committed
Allow arbitrary substrings in keep_together (#6)
1 parent b7daafa commit 74b1ed2

2 files changed

Lines changed: 35 additions & 10 deletions

File tree

casefy/casefy.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def camelcase(string: str) -> str:
88
"""Convert a string into camelCase.
99
1010
Args:
11-
string (:obj:`str`):
11+
string (:obj:`str`):
1212
The string to convert to camelCase.
1313
1414
Returns:
@@ -45,27 +45,45 @@ def snakecase(string: str, keep_together: List[str] = None) -> str:
4545
string (:obj:`str`):
4646
The string to convert to snake_case.
4747
keep_together (:obj:`List[str]`, `optional`):
48-
(Upper) characters to not split, e.g., "HTTP".
48+
Substrings to not split (case sensitive), e.g., "HTTP".
4949
5050
Returns:
5151
:obj:`str`: The snake_cased string.
5252
"""
5353
if not string:
5454
return ""
55+
if keep_together is None:
56+
keep_together = []
5557
leading_underscore: bool = string[0] == "_"
5658
trailing_underscore: bool = string[-1] == "_"
57-
# If all uppercase, turn into lowercase
59+
# If all uppercase, turn into lowercase, preserving the keep together in
60+
# upper case
5861
if string.isupper():
59-
string = string.lower()
60-
if keep_together:
6162
for keep in keep_together:
6263
string = string.replace(keep, f"_{keep.lower()}_")
64+
string = string.swapcase()
65+
if keep_together:
66+
# Here, we use "The Greatest Regex Trick Ever" here (see
67+
# https://www.rexegg.com/regex-best-trick.php)
68+
keep_pattern = rf"{'|'.join(f'({re.escape(keep)})' for keep in keep_together)}|"
69+
capturing_groups = r"".join(rf"\{i}" for i in range(1, len(keep_together) + 2))
70+
else:
71+
keep_pattern = r""
72+
capturing_groups = r"\1"
6373
# Manage separators
6474
string = re.sub(r"[\W]", "_", string)
6575
# Manage capital letters and numbers
66-
string = re.sub(r"([A-Z]|\d+)", r"_\1", string)
76+
string = re.sub(
77+
fr"{keep_pattern}([A-Z]|\d+)",
78+
fr"_{capturing_groups}",
79+
string
80+
)
6781
# Add "_" after numbers
68-
string = re.sub(r"(\d+)", r"\1_", string).lower()
82+
string = re.sub(
83+
fr"{keep_pattern}(\d+)",
84+
fr"{capturing_groups}_",
85+
string
86+
).lower()
6987
# Remove repeated "_"
7088
string = re.sub(r"[_]{2,}", "_", string)
7189
string = re.sub(r"^_", "", string) if not leading_underscore else string
@@ -130,6 +148,8 @@ def separatorcase(
130148
The string to convert.
131149
separator (:obj:`str`):
132150
The separator to use.
151+
keep_together (:obj:`List[str]`, `optional`):
152+
Substrings to not split (case sensitive), e.g., "HTTP".
133153
134154
Returns:
135155
:obj:`str`: The separator cased string.

tests/test_casefy.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def main():
5555
assert "foo_bar" == casefy.snakecase("FooBar")
5656
assert "foo_bar" == casefy.snakecase("FOO BAR")
5757
assert "foo_bar" == casefy.snakecase("foo bar")
58-
assert "foo_bar_baz" == casefy.snakecase("fooBARbaz", keep_together=["BAR"])
5958
assert "foo_bar_2" == casefy.snakecase("fooBar2")
6059
assert "foo_2_bar" == casefy.snakecase("foo2bar")
6160
assert "foo_2_bar" == casefy.snakecase("foo2Bar")
@@ -66,6 +65,10 @@ def main():
6665
assert "foo_bar" == casefy.snakecase("foo.bar")
6766
assert "_bar_baz" == casefy.snakecase("_bar_baz")
6867
assert "bar_baz" == casefy.snakecase(".bar_baz")
68+
assert "foo_bar_baz" == casefy.snakecase("fooBARbaz", keep_together=["BAR"])
69+
assert "foo_bar_v2" == casefy.snakecase("FooBarV2", keep_together=["V2"])
70+
assert "foo_bar_ids_v2" == casefy.snakecase("FooBarIDsV2", keep_together=["IDs", "V2"])
71+
assert "foobar_ids_v2" == casefy.snakecase("FooBarIDsV2", keep_together=["FooBar", "IDs", "V2"])
6972
assert "" == casefy.snakecase("")
7073
assert "" == casefy.snakecase(None)
7174

@@ -125,15 +128,17 @@ def main():
125128

126129
# separator case
127130
assert "foo.bar" == casefy.separatorcase("fooBar", separator=".")
128-
assert "foo/bar/baz" == casefy.separatorcase(
129-
"fooBARbaz", separator="/", keep_together=["BAR"])
130131
assert r"foo%bar%2" == casefy.separatorcase("fooBar2", separator="%")
131132
assert "foo&2&bar" == casefy.separatorcase("foo2Bar", separator="&")
132133
assert "foo?bar" == casefy.separatorcase("foo_bar", separator="?")
133134
assert "foo#bar" == casefy.separatorcase("foo-bar", separator="#")
134135
assert "foo=bar" == casefy.separatorcase("foo.bar", separator="=")
135136
assert "@bar@baz" == casefy.separatorcase("_bar_baz", separator="@")
136137
assert "#!bar#!baz" == casefy.separatorcase(".bar_baz", separator="#!")
138+
assert "foo/bar/baz" == casefy.separatorcase(
139+
"fooBARbaz", separator="/", keep_together=["BAR"])
140+
assert "foo|bar|v2" == casefy.separatorcase("FooBarV2", separator="|", keep_together=["V2"])
141+
assert "foo|bar|ids|v2" == casefy.separatorcase("FooBarIDsV2", separator="|", keep_together=["IDs", "V2"])
137142
assert "" == casefy.separatorcase("", separator="")
138143
assert "" == casefy.separatorcase(None, separator="")
139144

0 commit comments

Comments
 (0)