Repository: FactoryBoy/factory_boy (python)

Issue:
SQL Alchemy: TypeError: _save() got multiple values for argument 'session' when model has column named `session`
#### Description
We have a SQLAlchemy model which has a foreign key `session_id` and a relationship `session`. Whenever we try creating an instance with factory boy and pass in the session relationship to the factory we get an exception saying that multiple sessions were passed into the `_save` method in  `alchemy.py`. This makes sense to me but we would like it if this was not the case since everything was working fine in version `2.12`.

#### To Reproduce
##### Model / Factory code
```python
class MyModel(Base):
    session_id = Column(Integer, ForeignKey('session.id'), index=True, nullable=False)
    session = relationship("Session")

class BaseFactory(SQLAlchemyModelFactory):
    class Meta:
        abstract = True
        sqlalchemy_session = db.session   # the SQLAlchemy session object
        sqlalchemy_session_persistence = 'commit'

class MyModelFactory(BaseFactory):
    class Meta:
        model = MyModel

```

##### The issue
Instantiate your factory and pass in the session column kwarg
```python
MyModelFactory(session=some_session_object)
```

throw this error.
```python
/app/tests/test_models/test_session.py:424: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.tox/python/lib/python3.7/site-packages/factory/base.py:39: in __call__
    return cls.create(**kwargs)
.tox/python/lib/python3.7/site-packages/factory/base.py:527: in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
.tox/python/lib/python3.7/site-packages/factory/alchemy.py:51: in _generate
    return super(SQLAlchemyModelFactory, cls)._generate(strategy, params)
.tox/python/lib/python3.7/site-packages/factory/base.py:464: in _generate
    return step.build()
.tox/python/lib/python3.7/site-packages/factory/builder.py:283: in build
    kwargs=kwargs,
.tox/python/lib/python3.7/site-packages/factory/base.py:316: in instantiate
    return self.factory._create(model, *args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

cls = <class 'tests.factories.MyModelFactory'>
model_class = <class 'MyModel'>
args = ()
kwargs = {'session': Session 2}
session = <sqlalchemy.orm.scoping.scoped_session object at 0x10a668e50>

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """Create an instance of the model, and save it to the database."""
        session = cls._meta.sqlalchemy_session
    
        if session is None:
            raise RuntimeError("No session provided.")
        if cls._meta.sqlalchemy_get_or_create:
            return cls._get_or_create(model_class, session, *args, **kwargs)
>       return cls._save(model_class, session, *args, **kwargs)
E       TypeError: _save() got multiple values for argument 'session'

.tox/python/lib/python3.7/site-packages/factory/alchemy.py:99: TypeError

```

#### Notes
An easy solution might be to rename the param to sql_alchemy_session or something more explicit. I'm guessing this will happen to any model with any column named session no matter the type, it could be a simple int or string.


Pull request:
Avoid SQLAlchemyModelFactory name clash with a field named session
Internal helpers `_create` and `_get_or_create` expected to receive the
SQLAlchemy session as a keyword argument. Passing a value for a field
named session as a keyword argument to a SQLAlchemyModelFactory subclass
caused a name clash:

TypeError: got multiple values for keyword argument 'session'.

Fix #775

Patch:
diff --git a/CREDITS b/CREDITS
index caa7538..b7ba1ab 100644
--- a/CREDITS
+++ b/CREDITS
@@ -73,6 +73,7 @@ The project has received contributions from (in alphabetical order):
 * Samuel Paccoud <samuel@sampaccoud.com>
 * Saul Shanabrook <s.shanabrook@gmail.com>
 * Sean Löfgren <SeanBE@users.noreply.github.com>
+* Shahriar Tajbakhsh <shahriar@metaview.ai>
 * Tom <tom@tomleo.com>
 * alex-netquity <alex@netquity.com>
 * anentropic <ego@anentropic.com>
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 82fde40..913bd66 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -15,6 +15,10 @@ ChangeLog
 
     - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context.
 
+    - :issue:`775`: Change the signature for :meth:`~factory.alchemy.SQLAlchemyModelFactory._save` and
+      :meth:`~factory.alchemy.SQLAlchemyModelFactory._get_or_create` to avoid argument names clashes with a field named
+      ``session``.
+
 *Deprecated:*
 
     - :class:`~factory.django.DjangoModelFactory` will stop issuing a second call to
diff --git a/factory/alchemy.py b/factory/alchemy.py
index 2d24887..ef7a591 100644
--- a/factory/alchemy.py
+++ b/factory/alchemy.py
@@ -51,7 +51,7 @@ class SQLAlchemyModelFactory(base.Factory):
         return super()._generate(strategy, params)
 
     @classmethod
-    def _get_or_create(cls, model_class, session, *args, **kwargs):
+    def _get_or_create(cls, model_class, session, args, kwargs):
         key_fields = {}
         for field in cls._meta.sqlalchemy_get_or_create:
             if field not in kwargs:
@@ -66,7 +66,7 @@ class SQLAlchemyModelFactory(base.Factory):
 
         if not obj:
             try:
-                obj = cls._save(model_class, session, *args, **key_fields, **kwargs)
+                obj = cls._save(model_class, session, args, {**key_fields, **kwargs})
             except IntegrityError as e:
                 session.rollback()
                 get_or_create_params = {
@@ -95,11 +95,11 @@ class SQLAlchemyModelFactory(base.Factory):
         if session is None:
             raise RuntimeError("No session provided.")
         if cls._meta.sqlalchemy_get_or_create:
-            return cls._get_or_create(model_class, session, *args, **kwargs)
-        return cls._save(model_class, session, *args, **kwargs)
+            return cls._get_or_create(model_class, session, args, kwargs)
+        return cls._save(model_class, session, args, kwargs)
 
     @classmethod
-    def _save(cls, model_class, session, *args, **kwargs):
+    def _save(cls, model_class, session, args, kwargs):
         session_persistence = cls._meta.sqlalchemy_session_persistence
 
         obj = model_class(*args, **kwargs)


Test patch:
diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py
index 013a56a..8e76b5b 100644
--- a/tests/alchemyapp/models.py
+++ b/tests/alchemyapp/models.py
@@ -43,4 +43,11 @@ class NonIntegerPk(Base):
     id = Column(Unicode(20), primary_key=True)
 
 
+class SpecialFieldModel(Base):
+    __tablename__ = 'SpecialFieldModelTable'
+
+    id = Column(Integer(), primary_key=True)
+    session = Column(Unicode(20))
+
+
 Base.metadata.create_all(engine)
diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py
index 2205ca1..95e7f85 100644
--- a/tests/test_alchemy.py
+++ b/tests/test_alchemy.py
@@ -261,3 +261,34 @@ class SQLAlchemyNoSessionTestCase(unittest.TestCase):
         inst1 = NoSessionFactory.build()
         self.assertEqual(inst0.id, 0)
         self.assertEqual(inst1.id, 1)
+
+
+class NameConflictTests(unittest.TestCase):
+    """Regression test for `TypeError: _save() got multiple values for argument 'session'`
+
+    See #775.
+    """
+    def test_no_name_conflict_on_save(self):
+        class SpecialFieldWithSaveFactory(SQLAlchemyModelFactory):
+            class Meta:
+                model = models.SpecialFieldModel
+                sqlalchemy_session = models.session
+
+            id = factory.Sequence(lambda n: n)
+            session = ''
+
+        saved_child = SpecialFieldWithSaveFactory()
+        self.assertEqual(saved_child.session, "")
+
+    def test_no_name_conflict_on_get_or_create(self):
+        class SpecialFieldWithGetOrCreateFactory(SQLAlchemyModelFactory):
+            class Meta:
+                model = models.SpecialFieldModel
+                sqlalchemy_get_or_create = ('session',)
+                sqlalchemy_session = models.session
+
+            id = factory.Sequence(lambda n: n)
+            session = ''
+
+        get_or_created_child = SpecialFieldWithGetOrCreateFactory()
+        self.assertEqual(get_or_created_child.session, "")