Skip to content
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

Perf: Replace is_iterable function + use mapped pre-compiled Patterns #156

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all 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
29 changes: 9 additions & 20 deletions djangorestframework_camel_case/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
from collections import OrderedDict
from collections.abc import Iterable

from django.core.files import File
from django.http import QueryDict
Expand All @@ -11,6 +12,11 @@

camelize_re = re.compile(r"[a-z0-9]?_[a-z0-9]")

underscoreize_re_map = {
True: re.compile(r"([a-z0-9]|[A-Z]?(?=[A-Z](?=[a-z])))([A-Z])"),
False: re.compile(r"([a-z0-9]|[A-Z]?(?=[A-Z0-9](?=[a-z0-9]|(?<![A-Z])$)))([A-Z]|(?<=[a-z])[0-9](?=[0-9A-Z]|$)|(?<=[A-Z])[0-9](?=[0-9]|$))")
}


def underscore_to_camel(match):
group = match.group()
Expand Down Expand Up @@ -48,21 +54,13 @@ def camelize(data, **options):
else:
new_dict[new_key] = result
return new_dict
if is_iterable(data) and not isinstance(data, str):
if isinstance(data, Iterable) and not isinstance(data, str):
return [camelize(item, **options) for item in data]
return data


def get_underscoreize_re(options):
if options.get("no_underscore_before_number"):
pattern = r"([a-z0-9]|[A-Z]?(?=[A-Z](?=[a-z])))([A-Z])"
else:
pattern = r"([a-z0-9]|[A-Z]?(?=[A-Z0-9](?=[a-z0-9]|(?<![A-Z])$)))([A-Z]|(?<=[a-z])[0-9](?=[0-9A-Z]|$)|(?<=[A-Z])[0-9](?=[0-9]|$))"
return re.compile(pattern)
Comment on lines -56 to -61
Copy link
Author

@PaarthShah PaarthShah May 18, 2024

Choose a reason for hiding this comment

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

About a 10% improvement for the first pattern, and 27% for the second, at least on my machine.
The change I made allows the pattern to be compiled exactly once on module instantiation, rather than repeatedly every time this function is entered.

Before:

from djangorestframework_camel_case.util import camel_to_underscore
timeit.timeit(lambda: camel_to_underscore("someName"))
2.591615671000909
timeit.timeit(lambda: camel_to_underscore("someName5", some_number_before_number=True))
2.839183228003094

After:

import timeit
from djangorestframework_camel_case.util import camel_to_underscore
timeit.timeit(lambda: camel_to_underscore("someName"))
2.3492436910019023
timeit.timeit(lambda: camel_to_underscore("someName5", some_number_before_number=True))
2.227818359999219



def camel_to_underscore(name, **options):
underscoreize_re = get_underscoreize_re(options)
underscoreize_re = underscoreize_re_map[options.get("no_underscore_before_number", False)]
return underscoreize_re.sub(r"\1_\2", name).lower().lstrip("_")


Expand Down Expand Up @@ -104,16 +102,7 @@ def underscoreize(data, **options):
new_query.setlist(key, value)
return new_query
return new_dict
if is_iterable(data) and not isinstance(data, (str, File)):
if isinstance(data, Iterable) and not isinstance(data, (str, File)):
return [underscoreize(item, **options) for item in data]

return data


def is_iterable(obj):
try:
iter(obj)
except TypeError:
return False
else:
return True