question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

The unreleased `Transformer` has a conflicting API with the overall design

See original GitHub issue

Description

For most declarations, I can override the default mechanism by passing an explicit value:

>>> UserFactory().email
"john.doe@example.org"
>>> UserFactory(email="jane.doe@example.org").email
"jane.doe@example.org"

If the factory uses a factory.Transformer field, I can’t force a value:

>>> UserFactory().password
"pbkdf2_sha256$216000$Gj2b9f0Ln1ms$1dBlLEYrtGiwEA219JU831VAdscD2f9nFY37PrNfCDU="
>>> UserFactory(
...     password="pbkdf2_sha256$216000$Gj2b9f0Ln1ms$1dBlLEYrtGiwEA219JU831VAdscD2f9nFY37PrNfCDU="
... ).password
"pbkdf2_sha256$216000$EUKlFdNViyB5$vcfPaK6H7pDiQAa9ZIbFoj5oj55tUyHKHDUfxI4GIeY="

As a user, it is quite confusing to have very different behaviours for fields; if I have a specific password hash (in this example), I need to have a way to set it.

Next steps

I’m not sure we can officially release the factory.Transformer (and related code) without addressing this topic first; how can a user bypass the transform?

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:1
  • Comments:8 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
francoisfreitagcommented, Oct 21, 2022

Thanks for sharing the detailed analysis!

I agree that changing factory.Transformer to accept a pre- declaration would be an improvement, happy to give that a shot.

However, I am against the post- declaration part: the Transformer is meant to transform the value before to instantiate the object. This declaration was specifically introduced to deal with #366, which was a blocker for #316. My concern with a post-generation would be to keep encouraging the (bad) practice of saving the generated object twice with the CREATE strategy. If a post- declaration is allowed, an additional save is needed for the object in database to match the Python instance, going against #316. Unless a clear use case is presented, where Transformer would be a real gain, I’m convinced it’s better not to implement it.

Enrich it, where a user can provide a forced value to name = factory.Transformer(…) with name__ = x

This syntax is very unclear to me. It’s also not documented, not tested and does not appear a single time in the library repo. I would rather consider it an implementation detail of the library, and not start advertising it or even commit to supporting it. A GitHub search (although difficult because the = sign is ignored) does not yield usage either. Raw (or Force) could be declared inner to Transformer, so that users need to specify Transformer.Raw to wrap the value, clarifying its scope and preventing confusion for Trait or Maybe:

class Transformer:
    class Raw:
         # Raw implementation
    # Transformer implementation

What do you think?

1reaction
rbarroiscommented, Sep 24, 2022

I’d like to move this forward; here are a couple of additional considerations we should take into account 😉

Requirements

With the following code (mixing examples from @francoisfreitag and @n1ngu):

class UserFactory(factory.Factory):
    fullname = factory.Transformer(str.upper, "Joan Deer")
    password = factory.Transformer(make_password, "password123")

I believe we want the following options to be possible:

  • Providing a value generator (i.e another declaration) and having the transformation apply to it;
  • Forcing a specific value;
  • Reduce the risk of users “losing” track of the raw value that was provided to the transform (in @n1ngu’s example, can we keep track of the pre-upper() name?).

Solving these would also address the topic discussed in #963.

Current behaviour

With the 3.2.1 code, the user can rely on assert MyFactory(field=value).field == value, except whe the factory performs some mangling through Factory._meta.rename.

In that situation, achieving the goals of Transformer would require:

class UserFactory(factory.Factory):
  class Meta:
    name_raw = factory.Faker("fullname")
  name = factory.LazyAttribute(lambda o: o.name_raw.upper())

The user would then use:

  • UserFactory(name_raw="value") to force a specific pre-transformation value;
  • UserFactory(name="value") to force a specific final value

Alternative implementations

Other options, for instance suggested in #963, would be to declare the transformation as part of the LazyAttribute or SelfAttribute:

class UserFactory(factory.Factory):
  class Meta:
    name_raw = factory.Faker("fullname")
 name = factory.SelfAttribute("name_raw", transform=str.upper)

However, that pattern would require the addition of a transform= keyword argument to all declarations, which would be cumbersome and might conflict with named arguments expected by other declarations (e.g. SubFactory).

Declarations with similar issues

The following declarations exhibit a similar issue to the proposed factory.Transformer:

<dl> <dt>factory.Trait</dt> <dd>Once the class declaration of a factory using a `Trait` has been parsed by Python, the `Trait` is converted into a set of `Maybe`; callers cannot provide a new value for the trait afterwards, or "delete" the `Maybe` portion.</dd> <dt>factory.PostGeneration</dt> <dd>Those declarations have specific handling: if the caller provides a non-declaration value, it is provided to the function. If, instead, a factory post-generation declaration is provided, it overrides the declaration. </dd> <dt>factory>RelatedFactory</dt> <dd>Similar to `factory.PostGeneration`, a user can bypass a call to a related factory by providing a nake value to the factory: `UserFactory(main_group=SomeGroup)`. If, instead, they provide another `RelatedFactory` declaration, that value would override the existing `RelatedFactory` generation: `UserFactory(main_group=factory.RelatedFactory(LimitedGroupFactory))` </dd> </dl>

Possible options

Special declaration

As proposed in #888, and similarly to the implementation for factory.PostGeneration, we could special-case a declaration:

>>> UserFactory(name="Joan Doer").name
"JOAN DOER"
>>> UserFactory(name=factory.Force("Joan Doer")).name
"Joan Doer"

Specific keyword argument

We could support an explicit keyword argument to force the raw value:

>>> UserFactory(name="Joan Doer").name
"JOAN DOER"
>>> UserFactory(name__="Joan Doer").name
"Joan Doer"

Discussion

The “special declaration” case requires adding a new kwarg, which is only useful for the factory.Transformer case.

  • It wouldn’t make sense for factory.Trait — as the Trait declaration has already been parsed at class declaration time.
  • It wouldn’t make sense for factory.PostGeneration, as those declarations are supposed to alter the instance, not provide a field.
  • It would provide a duplicated API for bypassing factory.RelatedFactory, where one could call either UserFactory(main_group=SomeGroup) or UserFactory(main_group=factory.Force(SomeGroup)) to bypass the related factory — although the latter might be clearer.

The “specific keyword” pattern is similar to the behaviour used by @post_generation and PostGenerationMethodCall:

  • The user can call UserFactory(main_group__=SomeGroup) to bypass a main_group = factory.RelatedFactory(...) declaration,;
  • They can call UserFactory(make_password__="hunter2").

Additional considerations

What happens with the following factories?

class UserFactory(factory.Factory):
  credentials = factory.Dict(
    password=factory.Transformer(make_password, "hunter2),
  )

class userFactory(factory.Factory):
  class Params:
    with_strong_password = factory.Trait(password="A Very Strong Password !!")
  password = factory.Transformer(make_password, "hunter2")

class TenantFactory(factory.Factory):
  name = factory.Faker("company_name")
  register_main_dns = factory.PostGenerationMethodCall("add_dns_alias")
  register_main_dns__ = factory.Transformer(slugify_dns, factory.SelfAttribute("name"))

Conclusion

I believe the best way forward is:

  • Fix the factory.Transformer declaration to be able to accept either a pre- or post- declaration, as Maybe and Trait do;
  • Enrich it, where a user can provide a forced value to name = factory.Transformer(...) with name__ = x
Read more comments on GitHub >

github_iconTop Results From Across the Web

Unreleased toy - Transformers Wiki
Unicron wanted a toy, but his bargaining posture was highly dubious. Two Unicron prototypes were created during G1, neither saw full release. Diaclone...
Read more >
docs/advanced-development.md · dev · subugoe / Theology ...
Changes to third party libraries / unreleased dependencies. Some requirements could be only ... This API has some simple design flaws, it's not...
Read more >
Plux: A Higher-level Plugin Mechanism Around Python's Entry Point ...
PluginManager : manages the run time lifecycle of a Plugin, which has three states: resolved: the entrypoint pointing to the PluginSpec was imported...
Read more >
Q7A: Questions and Answers
What do you expect from audits of API suppliers by drug product manufacturers? With Q7A we now have a GMP guidance document that...
Read more >
Mental conflict - Imaginations and scary | OpenSea
... permeated car despite having conditioning recycled tenth time ive arrested selling deepfried cigars encountered maize thought incredibly corny miniature ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found