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

support a fully qualified path string as Meta.model #944

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ Meta options
.. attribute:: model

This optional attribute describes the class of objects to generate.
It could be a class or the fully qualified import path to it.

If unset, it will be inherited from parent :class:`Factory` subclasses.

.. versionadded:: 2.4.0

.. versionadded:: 3.3
Support fully qualified import path to the class

.. method:: get_model_class()

Returns the actual model class (:attr:`FactoryOptions.model` might be the
Expand Down
2 changes: 1 addition & 1 deletion factory/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def get_model_class(self):
This can be overridden in framework-specific subclasses to hook into
existing model repositories, for instance.
"""
return self.model
return utils.resolve_type(self.model) if isinstance(self.model, str) else self.model

def __str__(self):
return "<%s for %s>" % (self.__class__.__name__, self.factory.__name__)
Expand Down
30 changes: 10 additions & 20 deletions factory/declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,31 +343,21 @@ class _FactoryWrapper:
path for that subclass (e.g 'myapp.factories.MyFactory').
"""
def __init__(self, factory_or_path):
self.factory = None
self.module = self.name = ''
if isinstance(factory_or_path, type):
self.factory = factory_or_path
else:
if not (isinstance(factory_or_path, str) and '.' in factory_or_path):
raise ValueError(
"A factory= argument must receive either a class "
"or the fully qualified path to a Factory subclass; got "
"%r instead." % factory_or_path)
self.module, self.name = factory_or_path.rsplit('.', 1)

if not (isinstance(factory_or_path, type) or (isinstance(factory_or_path, str) and '.' in factory_or_path)):
raise ValueError(
"A factory= argument must receive either a class "
"or the fully qualified path to a Factory subclass; got "
"%r instead." % factory_or_path)
self.factory = factory_or_path

def get(self):
if self.factory is None:
self.factory = utils.import_object(
self.module,
self.name,
)
if isinstance(self.factory, str):
self.factory = utils.resolve_type(self.factory)
return self.factory

def __repr__(self):
if self.factory is None:
return f'<_FactoryImport: {self.module}.{self.name}>'
else:
return f'<_FactoryImport: {self.factory.__class__}>'
return f'<_FactoryImport: {self.factory}>'


class SubFactory(BaseDeclaration):
Expand Down
8 changes: 8 additions & 0 deletions factory/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ def import_object(module_name, attribute_name):
return getattr(module, attribute_name)


def resolve_type(type_or_path):
Copy link
Member

Choose a reason for hiding this comment

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

resolve_type is always called with an str. Can simplify the function.

if isinstance(type_or_path, type):
return type_or_path
if not (isinstance(type_or_path, str) and '.' in type_or_path):
raise ValueError("Must receive either an object or the fully qualified path")
return import_object(*type_or_path.rsplit('.', 1))


class log_pprint:
"""Helper for properly printing args / kwargs passed to an object.

Expand Down
11 changes: 11 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright: See the LICENSE file.

import mailbox
import unittest

from factory import base, declarations, enums, errors
Expand Down Expand Up @@ -215,6 +216,16 @@ class Meta:
with self.assertRaises(TypeError):
type("SecondFactory", (base.Factory,), {"Meta": Meta})

def test_meta_model_as_path(self):
class MailboxFactory(base.Factory):
class Meta:
model = "mailbox.Mailbox"
path = "/tmp/mail"

box = MailboxFactory()
assert isinstance(box, mailbox.Mailbox)
mgaitan marked this conversation as resolved.
Show resolved Hide resolved
assert box._path == "/tmp/mail"


class DeclarationParsingTests(unittest.TestCase):
def test_classmethod(self):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def test_path(self):

def test_lazyness(self):
f = declarations._FactoryWrapper('factory.declarations.Sequence')
self.assertEqual(None, f.factory)
self.assertEqual('factory.declarations.Sequence', f.factory)

factory_class = f.get()
self.assertEqual(declarations.Sequence, factory_class)
Expand All @@ -205,7 +205,7 @@ def test_cache(self):
"""Ensure that _FactoryWrapper tries to import only once."""
orig_date = datetime.date
w = declarations._FactoryWrapper('datetime.date')
self.assertEqual(None, w.factory)
self.assertEqual('datetime.date', w.factory)

factory_class = w.get()
self.assertEqual(orig_date, factory_class)
Expand Down
23 changes: 19 additions & 4 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright: See the LICENSE file.


import datetime
import itertools
import unittest

Expand All @@ -10,9 +10,7 @@
class ImportObjectTestCase(unittest.TestCase):
def test_datetime(self):
imported = utils.import_object('datetime', 'date')
import datetime
d = datetime.date
self.assertEqual(d, imported)
self.assertEqual(datetime.date, imported)

def test_unknown_attribute(self):
with self.assertRaises(AttributeError):
Expand All @@ -23,6 +21,23 @@ def test_invalid_module(self):
utils.import_object('this-is-an-invalid-module', '__name__')


class ResolveTypeTestCase(unittest.TestCase):
def test_datetime(self):
imported = utils.resolve_type('datetime.date')
self.assertEqual(datetime.date, imported)

def test_unknown_attribute(self):
with self.assertRaises(AttributeError):
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
with self.assertRaises(AttributeError):
with self.assertRaisesRegex(AttributeError, r"^module 'datetime' has no attribute 'foo'$"):

utils.resolve_type('datetime.foo')

def test_invalid_module(self):
with self.assertRaises(ImportError):
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
with self.assertRaises(ImportError):
with self.assertRaisesRegex(ImportError, r"^No module named 'this-is-an-invalid-module'$"):

utils.resolve_type('this-is-an-invalid-module.__name__')

def test_is_a_class(self):
return utils.resolve_type(datetime.date) is datetime.date


class LogPPrintTestCase(unittest.TestCase):
def test_nothing(self):
txt = str(utils.log_pprint())
Expand Down