factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

Extend `SelfAttribute` with some `callback` kwarg, for convenience

Open flaeppe opened this issue 1 year ago • 4 comments

The problem

Quite a lot of times I'd like to refine a value that can be accessed with a SelfAttribute. This can normally be done by combining it with a LazyAttribute and 1 or 2 param variables, but it'd be quite convenient if that was a shortcut so one didn't have to write that out explicitly every time. And also avoid having to either invent new variable names or prefix them to avoid collisions.

I also think, in general, that it would be a nice extension for SelfAttribute to expose a way to intercept the picked up value before it's assigned. A bit similar to what LazyAttribute by itself does (though on the factory instance).

Consider this simple case:

class DataFactory(factory.DictFactory):
    coordinates = factory.Dict(
        {
            "lat": factory.SelfAttribute("..latitude"),
            "lng": factory.SelfAttribute("..longitude"),
        }
    )

    class Params:
        # Faker outputs latitude and longitude as `Decimal`, I want it as `float`
        lat = factory.Faker("latitude")
        lng = factory.Faker("longitude")
        latitude = factory.LazyAttribute(lambda obj: float(obj.lat))
        longitude = factory.LazyAttribute(lambda obj: float(obj.lng))

Which could then become

class DataFactory(factory.DictFactory):
    coordinates = factory.DictFactory(
        {
            "lat": factory.SelfAttribute("..latitude", callback=float),
            "lng": factory.SelfAttribute("..longitude", callback=float),
        }
    )

    class Params:
        latitude = factory.Faker("latitude")
        longitude = factory.Faker("longitude")

Extra notes

  • I'm aware that I could place and evaluate the Faker instance inside of the LazyAttribute function, but it feels like this could be supported by the public API, rather than having to resort to a private API.
  • Better naming suggestions for the "callback" kwarg are welcome (one suggestion I have is coerce)

flaeppe avatar Jul 14 '22 11:07 flaeppe

Seems to me, that you want the ability to be able to change the latitude and longitude at will AND also have them be cast to float; all while having a "compressed" / easy to understand package:

class DataFactory(factory.DictFactory):
    coordinates = factory.Dict(
        {
            # Faker outputs latitude and longitude as `Decimal`, I want it as `float`
            "lat": factory.LazyAttribute(lambda obj: float(obj.factory_parent.latitude)),
            "lng": factory.LazyAttribute(lambda obj: float(obj.factory_parent.longitude)),
        }
    )

    class Params:
        latitude = factory.Faker("latitude")
        longitude = factory.Faker("longitude")

Thus when calling it DataFactory() yields random coordinates via Faker cast-ed into float. Random example:

>>> DataFactory()
{'coordinates': {'lat': -23.7679435, 'lng': -91.710909}}

And still, have the option to do:

>>> DataFactory(latitude=1, longitude=2)
{'coordinates': {'lat': 1.0, 'lng': 2.0}}

kingbuzzman avatar Jul 18 '22 10:07 kingbuzzman

Ah, yeah. I've missed that factory_parent is part of the API

flaeppe avatar Jul 18 '22 10:07 flaeppe

I guess this addition in the end is a shortcut/convenience method as/if the LazyAttribute approach becomes a bit verbose

flaeppe avatar Jul 18 '22 10:07 flaeppe

Like I said before, I'm a negative 0 on this. While I don't like more than 1 way to do something, I won't stand in the way of your PR. I accepted it ;) -- let's see what the maintainers say.

kingbuzzman avatar Jul 18 '22 10:07 kingbuzzman

I’m not sold on the use case. Why not change the data source to generate the data type of interest? Here, that could mean extending Faker if the use case is general enough, or writing helper for your project that generates float longitude and lattitude (maybe from Faker Decimal values).

francoisfreitag avatar Aug 26 '22 15:08 francoisfreitag

class Data(factory.DictFactory):
     coordinates = factory.Dict({
         "lat": factory.Faker("pyfloat", max_value=90, min_value=-90),
         "lng": factory.Faker("pyfloat", max_value=180, min_value=-180),
     })

Indeed, composing values from params and other declarations is sometimes cumbersome, but I am not a big fan of this proposal either.

n1ngu avatar Sep 17 '22 00:09 n1ngu

Not much interest has been shown for this proposal, we won’t be moving forward with it in the short term. Thank you for taking the time to submit it, we’ll happily review new proposals in the future!

francoisfreitag avatar Sep 17 '22 15:09 francoisfreitag