Skip to content

Don't depend on stringcase (closes #369) #375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from ...client import Client
from ...models.test_inline_objects_json_body import TestInlineObjectsJsonBody
from ...models.test_inline_objects_response_200 import TestInlineObjectsResponse_200
from ...models.test_inline_objects_response200 import TestInlineObjectsResponse200
from ...types import Response


Expand All @@ -29,15 +29,15 @@ def _get_kwargs(
}


def _parse_response(*, response: httpx.Response) -> Optional[TestInlineObjectsResponse_200]:
def _parse_response(*, response: httpx.Response) -> Optional[TestInlineObjectsResponse200]:
if response.status_code == 200:
response_200 = TestInlineObjectsResponse_200.from_dict(response.json())
response_200 = TestInlineObjectsResponse200.from_dict(response.json())

return response_200
return None


def _build_response(*, response: httpx.Response) -> Response[TestInlineObjectsResponse_200]:
def _build_response(*, response: httpx.Response) -> Response[TestInlineObjectsResponse200]:
return Response(
status_code=response.status_code,
content=response.content,
Expand All @@ -50,7 +50,7 @@ def sync_detailed(
*,
client: Client,
json_body: TestInlineObjectsJsonBody,
) -> Response[TestInlineObjectsResponse_200]:
) -> Response[TestInlineObjectsResponse200]:
kwargs = _get_kwargs(
client=client,
json_body=json_body,
Expand All @@ -67,7 +67,7 @@ def sync(
*,
client: Client,
json_body: TestInlineObjectsJsonBody,
) -> Optional[TestInlineObjectsResponse_200]:
) -> Optional[TestInlineObjectsResponse200]:
""" """

return sync_detailed(
Expand All @@ -80,7 +80,7 @@ async def asyncio_detailed(
*,
client: Client,
json_body: TestInlineObjectsJsonBody,
) -> Response[TestInlineObjectsResponse_200]:
) -> Response[TestInlineObjectsResponse200]:
kwargs = _get_kwargs(
client=client,
json_body=json_body,
Expand All @@ -96,7 +96,7 @@ async def asyncio(
*,
client: Client,
json_body: TestInlineObjectsJsonBody,
) -> Optional[TestInlineObjectsResponse_200]:
) -> Optional[TestInlineObjectsResponse200]:
""" """

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@
from .model_with_union_property_inlined_fruit_type0 import ModelWithUnionPropertyInlinedFruitType0
from .model_with_union_property_inlined_fruit_type1 import ModelWithUnionPropertyInlinedFruitType1
from .test_inline_objects_json_body import TestInlineObjectsJsonBody
from .test_inline_objects_response_200 import TestInlineObjectsResponse_200
from .test_inline_objects_response200 import TestInlineObjectsResponse200
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another issue 😞. I imagine this is why all of the existing string case libraries get it wrong... so many edge cases. I'll add a unit test for this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's exactly what I was talking about in my previous comment.

from .validation_error import ValidationError
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

from ..types import UNSET, Unset

T = TypeVar("T", bound="TestInlineObjectsResponse_200")
T = TypeVar("T", bound="TestInlineObjectsResponse200")


@attr.s(auto_attribs=True)
class TestInlineObjectsResponse_200:
class TestInlineObjectsResponse200:
""" """

a_property: Union[Unset, str] = UNSET
Expand All @@ -28,8 +28,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
a_property = d.pop("a_property", UNSET)

test_inline_objects_response_200 = cls(
test_inline_objects_response200 = cls(
a_property=a_property,
)

return test_inline_objects_response_200
return test_inline_objects_response200
3 changes: 0 additions & 3 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ strict_equality = True
[mypy-importlib_metadata]
ignore_missing_imports = True

[mypy-stringcase]
ignore_missing_imports = True

[mypy-typer]
ignore_missing_imports = True

32 changes: 20 additions & 12 deletions openapi_python_client/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import builtins
import re
from keyword import iskeyword
from typing import List

import stringcase
delimiters = " _-"


def sanitize(value: str) -> str:
""" Removes every character that isn't 0-9, A-Z, a-z, ' ', -, or _ """
return re.sub(r"[^\w _\-]+", "", value)
""" Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter """
return re.sub(rf"[^\w{delimiters}]+", "", value)


def split_words(value: str) -> List[str]:
""" Split a string on non-capital letters and known delimiters """
value = " ".join(re.split("([A-Z]?[a-z0-9]+)", value))
return re.findall(rf"[^{delimiters}]+", value)


def fix_keywords(value: str) -> str:
Expand All @@ -25,22 +32,23 @@ def fix_reserved_words(value: str) -> str:
return value


def group_title(value: str) -> str:
value = re.sub(r"([A-Z]{2,})([A-Z][a-z]|[ \-_]|$)", lambda m: m.group(1).title() + m.group(2), value.strip())
value = re.sub(r"(^|[ _-])([A-Z])", lambda m: m.group(1) + m.group(2).lower(), value)
return value


def snake_case(value: str) -> str:
return fix_keywords(stringcase.snakecase(group_title(sanitize(value))))
words = split_words(sanitize(value))
value = "_".join(words).lower()
return fix_keywords(value)


def pascal_case(value: str) -> str:
return fix_keywords(stringcase.pascalcase(sanitize(value.replace(" ", ""))))
words = split_words(sanitize(value))
capitalized_words = (word.capitalize() if not word.isupper() else word for word in words)
value = "".join(capitalized_words)
return fix_keywords(value)


def kebab_case(value: str) -> str:
return fix_keywords(stringcase.spinalcase(group_title(sanitize(value))))
words = split_words(sanitize(value))
value = "-".join(words).lower()
return fix_keywords(value)


def remove_string_escapes(value: str) -> str:
Expand Down
48 changes: 2 additions & 46 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ include = ["CHANGELOG.md", "openapi_python_client/py.typed"]
[tool.poetry.dependencies]
python = "^3.6"
jinja2 = "^2.11.1"
stringcase = "^1.2.0"
typer = "^0.3"
colorama = {version = "^0.4.3", markers = "sys_platform == 'win32'"}
shellingham = "^1.3.2"
Expand Down
8 changes: 4 additions & 4 deletions tests/test_parser/test_properties/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ def test_class_from_string_default_config():
@pytest.mark.parametrize(
"class_override, module_override, expected_class, expected_module",
(
(None, None, "_MyResponse", "_my_response"),
(None, None, "MyResponse", "my_response"),
("MyClass", None, "MyClass", "my_class"),
("MyClass", "some_module", "MyClass", "some_module"),
(None, "some_module", "_MyResponse", "some_module"),
(None, "some_module", "MyResponse", "some_module"),
),
)
def test_class_from_string(class_override, module_override, expected_class, expected_module):
from openapi_python_client.config import ClassOverride, Config
from openapi_python_client.parser.properties import Class

ref = "#/components/schemas/_MyResponse"
ref = "#/components/schemas/MyResponse"
config = Config(
class_overrides={"_MyResponse": ClassOverride(class_name=class_override, module_name=module_override)}
class_overrides={"MyResponse": ClassOverride(class_name=class_override, module_name=module_override)}
)

result = Class.from_string(string=ref, config=config)
Expand Down
4 changes: 4 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def test_snake_case_from_pascal_with_acronyms():
assert utils.snake_case("HTTPResponse") == "http_response"
assert utils.snake_case("APIClientHTTPResponse") == "api_client_http_response"
assert utils.snake_case("OAuthClientHTTPResponse") == "o_auth_client_http_response"
assert utils.snake_case("S3Config") == "s3_config"


def test_snake_case_from_pascal():
Expand All @@ -20,6 +21,7 @@ def test_snake_case_from_pascal():

def test_snake_case_from_camel():
assert utils.snake_case("httpResponseLowerCamel") == "http_response_lower_camel"
assert utils.snake_case("connectionID") == "connection_id"


def test_kebab_case():
Expand Down Expand Up @@ -59,6 +61,8 @@ def test_to_valid_python_identifier():
("snake_case", "SnakeCase"),
("TLAClass", "TLAClass"),
("Title Case", "TitleCase"),
("s3_config", "S3Config"),
("__LeadingUnderscore", "LeadingUnderscore"),
],
)
def test_pascalcase(before, after):
Expand Down