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

feature: Add possibility to map nested attributes #29

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Saurus119
Copy link

  • Added possililbity to map chained object reference easily when we create map between source and dest.

@anikolaienko
Copy link
Owner

Hi @Saurus119
I really appreciate your contribution. I promise to look into it as soon as I can, aprox. in the next 1-2 days.

README.md Outdated
mapper.add(
AdvancedUser, BasicUser, fields_mapping={
"name": "AdvancedUser.user.name",
"city": "AdvancedUser.user.city",
Copy link
Owner

Choose a reason for hiding this comment

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

The idea is good. Here are few things to consider:

  • If we know that the mapping is going to be from source class object, isn't name of the source class redundant?
    For instance, you can just say: "name": ".user.name" or "name": "$.user.name" where . or $. would mean that the value is mapped from the subfield of a source class.

  • The problem with string literal, is that there is no way to tell if "AdvancedUser.user.name" is a reference path to a source subfield or it is just a string literal. Meaning, what if someone wants the name field to have string literal value AdvancedUser.user.name, and not the subfield of a source object. This would be a unique scenario but not impossible.

There are couple of ways to solve the latter problem:

  1. (Preferred) Wrap the path in some sort of wrapper function to signify that this is a subfield path and not a string literal:
     "name": mapper.path("user.name"),  # where mapper.path would return an instance of MapPath wrapper class
# or just use MapPath class directly
     "name": MapPath("user.name")
  1. Have a different parameter for subfield mapping:
fields_mapping={
    "name": "just a string literal"
}, src_path={
    "city": "user.city"   # path to source object subfield
}

Solution (1) is preferrer because it will not break any existing usage of the library, where second approach modified the method signature and can break existing usage.

Copy link
Author

@Saurus119 Saurus119 Mar 6, 2025

Choose a reason for hiding this comment

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

I like the idea with MapPath another object that is specific for this scenario but at the first run I didn´t want to "overkill" the implementation but you are right with literal strings. I will try to adress till end of the weekend.

I was also considering this modifications:

  1. Change the register map from simple dict into MapperRegistry new class as this give opporunity to map one Src class to Multiple Dst if the field_mappings id different
  2. I didn´t see usecase where you are able map Iterables, so maybe add support for this as well as I code also in C# and I like the approach like: users= _mapper.Map<List<User>>(userEntities) which give me opportinity to map from my "DB" users to my for example data transfer objects easily with one line. And I am able to access Src and Dst object and thier property by reference and not the strings, example:
		CreateMap<SrcObject, DstObject>()
			.ForMember(dest => dest.Duration, opt => opt.MapFrom(dest.DurationSeconds))
			.ReverseMap();

Where ReverseMap means 1:1 mapping so object should have same fields. So after this function is called you just to same mapping as you have with map function,

But this two ideas will bring lot of code changes.

@Saurus119 Saurus119 marked this pull request as draft March 7, 2025 20:51
@anikolaienko anikolaienko changed the title feat: Add possibility to map nested attributes feature: Add possibility to map nested attributes Mar 9, 2025
Copy link
Owner

@anikolaienko anikolaienko left a comment

Choose a reason for hiding this comment

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

Great work! Added some comments, would like to hear your opinion.

Please be aware I've release version 2.2.0, very few minor changes, should not affect you, but recommend get the latest changes from main branch.

Also approved Github Actions on your branch. Now you should see automatic code check results and pytest results. I'm sure you are using pre-commit but for some reason mypy is failing on CI/CD. If you don't have pre-commit locally enabled for this repo, I recommend: https://pre-commit.com/

return self.attributes

def __repr__(self):
return f"MapPath({self.attributes})"
Copy link
Owner

Choose a reason for hiding this comment

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

This looks a bit complex. I honestly thought this would be just:

@dataclass
class MapPath:
    path: str

and in the file named map_path.py

T = TypeVar("T")
ClassifierFunction = Callable[[Type[T]], bool]
SpecFunction = Callable[[Type[T]], Iterable[str]]
FieldsMap = Optional[Dict[str | MapPath, Any]]
Copy link
Owner

Choose a reason for hiding this comment

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

I have enabled Github Actions to run checks on your branch and it seems to be failing.
Maybe makes sense for now to put these back as these are only used in 1 file. Not very confident in these TypeVar types that I've created a while ago. Need to refresh my memory.

f"Invalid path: '{path}'. Can´t reference to object attribute."
)

self._obj_prefix: str | None = None
Copy link
Owner

Choose a reason for hiding this comment

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

Is . really required? what if user just wants to map to source object property with different name?
E.g.

fields_mapping={
     "name": MapPath("user_name")    # where user_name is just a property in source object.
}

if not all(isinstance(map_path, MapPath) for map_path in map_paths):
raise MapPathMissMatchError(
"It is not allowed to mix MapPath mappings with string mappings."
)
Copy link
Owner

Choose a reason for hiding this comment

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

why not?

):
common_fields_mapping[target_obj_field] = self._rgetter(
obj, source_field[len(obj_type_prefix) :]
)
Copy link
Owner

Choose a reason for hiding this comment

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

here I would just leave getattr, as it's simple and easy to understand.

elif isinstance(source_field, MapPath):
common_fields_mapping[target_obj_field] = self._rgetter(
obj, source_field
)
Copy link
Owner

Choose a reason for hiding this comment

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

then here we know that we need to use recursive getter.
Could be also resolve_map_path or get_by_map_path or something similar, little bit more descriptive.
You'll know that you pass in a source_field of type MapPath and this function will get you a value using this MapPath.

If the value not possible to retrieve, I would suggest this function raises an InvalidMapPathError. So this is something user may expect from our package. Currently the reduce function in _rgetter would fail with unknown error.

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

Successfully merging this pull request may close these issues.

2 participants