Skip to content
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"bcrypt>=4,<6",
"whitenoise>=6.6,<7",
"python-dotenv>=1,<2",
"masonite-framework-orm>=3.1,<4",
"masonite-framework-orm[seeder]>=3.1,<4",
"sqids>=0.5,<1",
"cryptography>=46.0.7,<49",
"tldextract>=5,<6",
Expand Down
56 changes: 56 additions & 0 deletions src/masonite/commands/MakeFactoryCommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""New Factory Command."""
import inflection
import os

from ..utils.filesystem import make_directory, render_stub_file, get_module_dir
from ..utils.location import factories_path, models_path
from .Command import Command


class MakeFactoryCommand(Command):
"""
Creates a new model factory.

factory
{name : Name of the factory}
{--m|model=? : The model this factory builds, defaults to the factory name}
{--f|force=? : Force overriding file if already exists}
"""

def __init__(self, application):
super().__init__()
self.app = application

def handle(self):
name = inflection.camelize(self.argument("name"))
if not name.endswith("Factory"):
name += "Factory"

model = inflection.camelize(self.option("model") or name[: -len("Factory")])
model_module = (
models_path(model, absolute=False).replace("/", ".").replace("\\", ".")
)

content = render_stub_file(self.get_factories_path(), name)
content = content.replace("__model_module__", model_module)
content = content.replace("__model__", model)

filename = f"{name}.py"
filepath = factories_path(filename)
make_directory(filepath)
if os.path.exists(filepath) and not self.option("force"):
self.warning(
f"{filepath} already exists! Run the command with -f (force) to override."
)
return -1
with open(filepath, "w") as f:
f.write(content)

# add class to __init__.py
with open(os.path.join(os.path.dirname(filepath), "__init__.py"), "a") as f:
f.write(f"from .{name} import {name}\n")

self.info(f"Factory Created ({factories_path(filename, absolute=False)})")

def get_factories_path(self):
return os.path.join(get_module_dir(__file__), "../stubs/factories/Factory.py")
1 change: 1 addition & 0 deletions src/masonite/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .MakeJobCommand import MakeJobCommand
from .MakeRequestCommand import MakeRequestCommand
from .MakeMailableCommand import MakeMailableCommand
from .MakeFactoryCommand import MakeFactoryCommand
from .MakeProviderCommand import MakeProviderCommand
from .PublishPackageCommand import PublishPackageCommand
from .MakePolicyCommand import MakePolicyCommand
Expand Down
2 changes: 2 additions & 0 deletions src/masonite/foundation/Kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def register_commands(self) -> None:
MakeJobCommand,
MakeRequestCommand,
MakeMailableCommand,
MakeFactoryCommand,
MakeProviderCommand,
PublishPackageCommand,
MakeTestCommand,
Expand All @@ -62,6 +63,7 @@ def register_commands(self) -> None:
MakeJobCommand(self.application),
MakeRequestCommand(self.application),
MakeMailableCommand(self.application),
MakeFactoryCommand(self.application),
MakeProviderCommand(self.application),
PublishPackageCommand(self.application),
MakeTestCommand(self.application),
Expand Down
1 change: 1 addition & 0 deletions src/masonite/skeleton/app/Kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def register_database(self):

self.application.bind("migrations.location", "databases/migrations")
self.application.bind("seeds.location", "databases/seeds")
self.application.bind("factories.location", "databases/factories")

self.application.bind("resolver", config("database.db"))

Expand Down
Empty file.
21 changes: 21 additions & 0 deletions src/masonite/stubs/factories/Factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from masoniteorm.factories import Factory

from __model_module__ import __model__


class __class__:
"""Factory for the __model__ model.

Call `__class__.register()` once during app bootstrap (for example
from a service provider's `boot` method) to make this factory
available to `Factory(__model__).create()` / `.make()` in tests and
seeders.
"""

@staticmethod
def register():
Factory.register(__model__, __class__.definition)

@staticmethod
def definition(faker):
return {}
8 changes: 8 additions & 0 deletions src/masonite/utils/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,11 @@ def models_path(relative_path="", absolute=True):

The relative path can be returned instead by setting absolute=False."""
return _build_path("models.location", relative_path, absolute)


def factories_path(relative_path="", absolute=True):
"""Build the absolute path to the project factories directory or build the absolute path to a given
file relative to the project factories directory.

The relative path can be returned instead by setting absolute=False."""
return _build_path("factories.location", relative_path, absolute)
17 changes: 17 additions & 0 deletions tests/core/utils/test_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
jobs_path,
resources_path,
models_path,
factories_path,
)


Expand Down Expand Up @@ -115,3 +116,19 @@ def test_models_path(self):
self.assertEqual("tests/integrations/app/admin/Log.py", location)
location = models_path(absolute=False)
self.assertEqual(location, "tests/integrations/app/")

def test_factories_path(self):
location = factories_path("UserFactory.py")
self.assertTrue(
location.endswith("tests/integrations/databases/factories/UserFactory.py")
)
location = factories_path("package/PostFactory.py")
self.assertTrue(
location.endswith(
"tests/integrations/databases/factories/package/PostFactory.py"
)
)
location = factories_path("UserFactory.py", absolute=False)
self.assertEqual(
"tests/integrations/databases/factories/UserFactory.py", location
)
127 changes: 127 additions & 0 deletions tests/features/factories/test_make_factory_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import importlib
import os

from masoniteorm.factories import Factory

from tests import TestCase
from tests.integrations.app.User import User
from src.masonite.utils.location import factories_path


class TestMakeFactoryCommand(TestCase):
def _cleanup(self, target, init_file, original_init):
if os.path.exists(target):
os.remove(target)
with open(init_file, "w") as f:
f.write(original_init)

def test_command_creates_a_factory(self):
init_file = factories_path("__init__.py")
original_init = ""
if os.path.exists(init_file):
with open(init_file) as f:
original_init = f.read()

target = factories_path("FakeGeneratedFactory.py")
try:
self.craft("factory", "FakeGenerated")
self.assertTrue(os.path.exists(target))
with open(target) as f:
content = f.read()
self.assertIn("class FakeGeneratedFactory:", content)
self.assertIn(
"from tests.integrations.app.FakeGenerated import FakeGenerated",
content,
)
self.assertIn("Factory.register(FakeGenerated,", content)
finally:
self._cleanup(target, init_file, original_init)

def test_model_option_overrides_inferred_model(self):
init_file = factories_path("__init__.py")
original_init = ""
if os.path.exists(init_file):
with open(init_file) as f:
original_init = f.read()

target = factories_path("PostFactory.py")
try:
self.craft("factory", "Post --model=Article")
with open(target) as f:
content = f.read()
self.assertIn(
"from tests.integrations.app.Article import Article", content
)
self.assertIn("Factory.register(Article,", content)
finally:
self._cleanup(target, init_file, original_init)

def test_force_option_required_to_overwrite(self):
init_file = factories_path("__init__.py")
original_init = ""
if os.path.exists(init_file):
with open(init_file) as f:
original_init = f.read()

target = factories_path("FakeGeneratedFactory.py")
try:
self.craft("factory", "FakeGenerated")
self.craft("factory", "FakeGenerated").assertOutputContains(
"already exists"
)
self.craft("factory", "FakeGenerated --force").assertSuccess()
finally:
self._cleanup(target, init_file, original_init)

def test_generated_factory_can_make_and_create_real_records(self):
"""End-to-end: generate a factory, import it for real, and confirm it
actually produces (and persists) model instances via masoniteorm's
Factory engine -- not just that the file's text looks right."""
init_file = factories_path("__init__.py")
original_init = ""
if os.path.exists(init_file):
with open(init_file) as f:
original_init = f.read()

target = factories_path("UserFactory.py")
created_id = None
try:
self.craft("factory", "User").assertSuccess()

with open(target) as f:
content = f.read()
content = content.replace(
"return {}",
"return {\n"
' "name": faker.name(),\n'
' "email": faker.unique.email(),\n'
' "password": "secret",\n'
" }",
)
with open(target, "w") as f:
f.write(content)

module = importlib.import_module(
"tests.integrations.databases.factories.UserFactory"
)
importlib.reload(module)
module.UserFactory.register()

made = Factory(User).make()
self.assertIsInstance(made, User)
self.assertTrue(made.name)
# make() only builds the in-memory model, it never touches the
# database, so no primary key has been assigned yet.
self.assertNotIn("id", made.__attributes__)

created = Factory(User).create()
created_id = created.id
self.assertIsNotNone(created.id)

found = User.where("id", created.id).first()
self.assertIsNotNone(found)
self.assertEqual(found.email, created.email)
finally:
if created_id is not None:
User.where("id", created_id).delete()
self._cleanup(target, init_file, original_init)
3 changes: 3 additions & 0 deletions tests/integrations/app/Kernel/Kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ def register_database(self):
"migrations.location", "tests/integrations/databases/migrations"
)
self.application.bind("seeds.location", "tests/integrations/databases/seeds")
self.application.bind(
"factories.location", "tests/integrations/databases/factories"
)

self.application.bind("resolver", config("database.db"))

Expand Down
Empty file.