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

Add Support for letter_case #7

Closed
ZeldaZach opened this issue Sep 9, 2022 · 8 comments
Closed

Add Support for letter_case #7

ZeldaZach opened this issue Sep 9, 2022 · 8 comments

Comments

@ZeldaZach
Copy link

Upon using dataclasses_json, I was running into a performance issue (similar to lidatong/dataclasses-json#228) and looked for an alternative.

Your project seems to have the speed I'm looking for, but is missing a crucial component: The ability to filter using camelCase.

dataclasses_json supports camelCase via @dataclass_json(letter_case=LetterCase.CAMEL), but it doesn't seem that your project supports any kind of configurations. Was curious how feasible adding camelCase support would be.

Thanks for all the hard work!

@bprus
Copy link

bprus commented Sep 9, 2022

Exactly the same thoughts on my side. I started using this project a few days ago and what I'm missing the most is support for different casing :)

@cakemanny
Copy link
Owner

Hi, thanks for opening this issue.

I agree definitely agree that customisation could be expanded a little.
I want to discuss a little before implementing something for your use case, I hope that's ok.

So, there is a way do this already with per-field configuration option field_name:

from dataclasses import dataclass, field
from fastclasses_json import dataclass_json

@dataclass_json
@dataclass
class SnakesOfCamels:
    snake_one: int = field(metadata={"fastclasses_json": {"field_name": "snakeOne"}})
    snake_two: int = field(metadata={"fastclasses_json": {"field_name": "snakeTwo"}})
    snake_three: int = field(metadata={"fastclasses_json": {"field_name": "snakeThree"}})

SnakesOfCamels(1,2,3).to_dict()  # {'snakeOne': 1, 'snakeTwo': 2, 'snakeThree': 3}
SnakesOfCamels.from_dict({'snakeOne': 1, 'snakeTwo': 2, 'snakeThree': 3})  # SnakesOfCamels(snake_one=1, snake_two=2, snake_three=3)

The main problem here is that it's super repetitive, right?

In case you are looking for a quick hack, that should work most cases, you can write a simple decorator

Decorator Hack
from dataclasses import dataclass
from fastclasses_json import dataclass_json
import dataclasses

def to_camel_case(field_name):
    parts = field_name.split('_')
    return parts[0] + ''.join(map(lambda s: s.capitalize(), parts[1:]))


def camelise(cls):
    for field in dataclasses.fields(cls):
        field.metadata = {
            **field.metadata,
            "fastclasses_json": {
                **field.metadata.get("fastclasses_json", {}),
                "field_name": to_camel_case(field.name)
            }
        }
    return cls


@dataclass_json
@camelise
@dataclass
class SnakesOfCamels:
    snake_one: int
    snake_two: int
    snake_three: int

SnakesOfCamels(1,2,3).to_dict() # {'snakeOne': 1, 'snakeTwo': 2, 'snakeThree': 3}

Returning back to the main point

I don't plan to start including code for various naming conventions because there will be edge cases that many may have.

I think there are two different good options available which have their advantages and disadvantages. I'll put them as two subsequent comments so the Github reactions can be used on them perhaps.
...

@cakemanny
Copy link
Owner

  1. Allow users to specify function to transform the field names for a whole class
def your_field_name_rewrite(field_name):
    parts = field_name.split('_')
    return parts[0] + ''.join(map(lambda s: s.capitalize(), parts[1:]))


@dataclass_json(field_name_transform=your_field_name_rewrite)
@dataclass
class SnakesOfCamels:
    snake_one: int
    snake_two: int
    snake_three: int

@cakemanny
Copy link
Owner

cakemanny commented Sep 10, 2022

  1. Allow the transformation on per-field basis. This has the advantage that it's more flexible.
@dataclass_json
@dataclass
class SnakesOfCamels:
    snake_one: int = field(metadata={"fastclasses_json": {"field_name_transform": your_field_name_rewrite}})
    snake_two: int = field(metadata={"fastclasses_json": {"field_name_transform": your_field_name_rewrite}})
    snake_three: int = field(metadata={"fastclasses_json": {"field_name_transform": your_field_name_rewrite}})

In reality, most of the repetition can be factored out now in some way of your choosing e.g.

CAML_FIELD = {"fastclasses_json": {"field_name_transform": to_camel_case}}

@dataclass_json
@dataclass
class SnakesOfCamels:
    snake_one: int = field(metadata=CAMEL_FIELD)
    snake_two: int = field(metadata=CAMEL_FIELD)
    snake_three: int = field(metadata=CAMEL_FIELD)

corrected the factored version to not include arguments given to the decorator

@ZeldaZach
Copy link
Author

Your two options both have their advantages for sure! If we're only able to go with one approach, the first approach might be a bit more intuitive as folks normally have one key format for their classes (camel, snake, or other) -- I haven't worked with a Json blob that utilizes multiple different types, and that seems to be the exception and not the rule. Being able to designate a class as a whole feels the most natural, but I do feel for folks who might want to classify on a field-by-field basis... Since the original dataclasses-json was restrictive to class level and saw no complaints, I'd be most accepting of that kind of solution to have a better mirroring of the package.

@cakemanny
Copy link
Owner

I was thinking this through the other morning and I remembered why I have only put per-field configuration so far.

With the per-class configuration, it actually gets quite tricky, because the @fastclasses_json decorator is only required to be specified on the outer-most dataclass. When two different trees both share a dataclass class as a field type, then there can be two conflicting configurations to compile.

So, here's the example:

from dataclasses import dataclass, field
from fastclasses_json import dataclass_json

def to_camel_case(field_name):
    parts = field_name.split('_')
    return parts[0] + ''.join(map(lambda s: s.capitalize(), parts[1:]))

def to_proper_case(field_name):
    return ''.join(map(lambda s: s.capitalize(), field_name.split('_')))


@dataclass_json(field_name_transform=to_camel_case)
@dataclass
class Snakes:
    crowley: int
    kaa: int
    more_snakes: Adders

@dataclass_json(field_name_transform=to_proper_case)
@dataclass
class Mathematicians:
    euclid: int
    isaac_newton: int
    more_mathematicians: Adders

@dataclass
class Adders:
    pythagoras_of_samos: int

In dataclasses-json there's no problem here but in the fastclasses-json implementation there is a _fastclasses_json_to_dict compiled for Adders exactly once. So, the result you get would depend on which to_dict what called first, out of Mathematicians and Snakes.

>>> Snakes(1,2, Adders(3)).to_dict()
{'crowley': 1, 'kaa': 2, 'moreSnakes': {'pythagorasOfSamos': 3}}
>>> Mathematicians(1, 2, Adders(3)).to_dict()
{'Euclid': 1, 'IsaacNewton': 2, 'MoreMathematicians': {'pythagorasOfSamos': 3}}

or

>>> Mathematicians(1, 2, Adders(3)).to_dict()
{'Euclid': 1, 'IsaacNewton': 2, 'MoreMathematicians': {'PythagorasOfSamos': 3}}
>>> Snakes(1,2, Adders(3)).to_dict()
{'crowley': 1, 'kaa': 2, 'moreSnakes': {'PythagorasOfSamos': 3}}

I have some ideas, but I would need to explore them. Per-field configuration would be simpler to implement on the face of it.

Having a think 🤔

@ZeldaZach
Copy link
Author

Interesting corner case you bring up! I'm not opposed to per-field, just feels a bit redundant. You can also simply state that the top level on to_dict is the final factor that determines the case of all sub-classes, but it could be a bit harsh... I guess it really depends on how much extra effort would compiling each time add? Would it remove the "fast"ness of fastclasses-json? Or is there another alternative we can consider?

@cakemanny
Copy link
Owner

Sorry for the quietness.
I decided to work on the decorator argument version. Namely:

@dataclass_json(field_name_transform=your_field_name_rewrite)
@dataclass
class SnakesOfCamels:
   ...

I concede the per-field option would cause way too much needless repetition in large projects.

For the problem where the options can conflict between rival dataclasses above, I solve by including the hash of the transform function in the internal to/from_dict function name when it is generated. If multiple transforms are used, they compile separately.

I've published this in version 0.6.0

pip install fastclasses-json==0.6.0

and there's a simple new example added to the readme: #whole-tree-configuration-options

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants