Repository: projectmesa/mesa
Pull request #563 (2018-07-14 22:56:59)

Update and clean up cookiecutter layout 

coveralls: 
[![Coverage Status](https://coveralls.io/builds/17993339/badge)](https://coveralls.io/builds/17993339)

Coverage decreased (-0.3%) to 80.297% when pulling **b397eb392cdda0ce87ded1961535a7fe77bece5b on werwty:PR242** into **61893473d11a84ff68d805852294209f88488349 on projectmesa:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/17993339/badge)](https://coveralls.io/builds/17993339)

Coverage decreased (-0.3%) to 80.297% when pulling **b397eb392cdda0ce87ded1961535a7fe77bece5b on werwty:PR242** into **61893473d11a84ff68d805852294209f88488349 on projectmesa:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/17993339/badge)](https://coveralls.io/builds/17993339)

Coverage decreased (-0.3%) to 80.297% when pulling **b397eb392cdda0ce87ded1961535a7fe77bece5b on werwty:PR242** into **61893473d11a84ff68d805852294209f88488349 on projectmesa:master**.


Patch:
diff --git a/docs/best-practices.rst b/docs/best-practices.rst
index 1ee5d78a..b8885638 100644
--- a/docs/best-practices.rst
+++ b/docs/best-practices.rst
@@ -12,9 +12,6 @@ underscores, such as ``thunder_cats``. Within that directory:
 * ``README.md`` describes the model, how to use it, and any other details.
   Github will automatically show this file to anyone visiting the directory.
 
-* ``requirements.txt`` contains any additional Python distributions, beyond
-  Mesa itself, required to run the model.
-
 * ``model.py`` should contain the model class.  If the file gets large, it may
   make sense to move the complex bits into other files, but this is the first
   place readers will look to figure out how the model works.
@@ -23,7 +20,7 @@ underscores, such as ``thunder_cats``. Within that directory:
   class.
 
 * ``run.py`` is a Python script that will run the model when invoked via
-  ``mesa run``.
+  ``mesa runserver``.
 
 After the number of files grows beyond a half-dozen, try to use sub-folders to
 organize them. For example, if the visualization uses image files, put those in
@@ -33,6 +30,8 @@ The `Schelling
 <https://github.com/projectmesa/mesa/tree/master/examples/Schelling>`_ model is
 a good example of a small well-packaged model.
 
+It's easy to create a cookiecutter mesa model by running ``mesa startproject``
+
 Randomization
 -------------
 
diff --git a/docs/index.rst b/docs/index.rst
index 5111ae18..56b444d9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -40,11 +40,11 @@ Getting started quickly:
 
     $ pip install mesa
 
-To launch an example model, clone the `repository <https://github.com/projectmesa/mesa>`_ folder and invoke ``mesa run`` for one of the ``examples/`` subdirectories:
+To launch an example model, clone the `repository <https://github.com/projectmesa/mesa>`_ folder and invoke ``mesa runserver`` for one of the ``examples/`` subdirectories:
 
 .. code-block:: bash
 
-    schelling $ mesa run examples/wolf_sheep
+    schelling $ mesa runserver examples/wolf_sheep
 
 For more help on using Mesa, check out the following resources:
 
diff --git a/docs/overview.rst b/docs/overview.rst
index 09070ce7..592815a6 100644
--- a/docs/overview.rst
+++ b/docs/overview.rst
@@ -1,5 +1,5 @@
 Mesa Overview
-=========================================
+=============
 
 Mesa is a modular framework for building, analyzing and visualizing agent-based models.
 
@@ -7,7 +7,7 @@ Mesa is a modular framework for building, analyzing and visualizing agent-based
 
 
 Mesa Modules
--------------------
+------------
 
 Mesa is modular, meaning that its modeling, analysis and visualization components are kept separate but intended to work together. The modules are grouped into three categories:
 
@@ -64,6 +64,7 @@ If you instantiate a model and run it for one step, like so:
 
 You should see agents 0-4, activated in random order. See the `tutorial <tutorials/intro_tutorial.html>`_ or API documentation for more detail on how to add model functionality.
 
+To bootstrap a new model install mesa and run ``mesa startproject``
 
 Analysis modules
 ~~~~~~~~~~~~~~~~
diff --git a/examples/Schelling/README.md b/examples/Schelling/README.md
index 99a149e2..4dde87f5 100644
--- a/examples/Schelling/README.md
+++ b/examples/Schelling/README.md
@@ -16,10 +16,10 @@ To install the dependencies use pip and the requirements.txt in this directory.
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
diff --git a/examples/bank_reserves/Readme.md b/examples/bank_reserves/Readme.md
index 8b3da93f..386566ab 100644
--- a/examples/bank_reserves/Readme.md
+++ b/examples/bank_reserves/Readme.md
@@ -22,10 +22,10 @@ To install the dependencies use pip and the requirements.txt in this directory.
 
 ## Interactive Model Run
 
-To run the model interactively, use `mesa run` in this directory:
+To run the model interactively, use `mesa runserver` in this directory:
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/), select the model parameters, press Reset, then Start.
diff --git a/examples/boid-flockers/Readme.md b/examples/boid-flockers/Readme.md
index dba0cd9d..b197a50c 100644
--- a/examples/boid-flockers/Readme.md
+++ b/examples/boid-flockers/Readme.md
@@ -28,7 +28,7 @@ Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and p
 =======
 * Launch the visualization
 ```
-$ mesa run
+$ mesa runserver
 ```
 * Visit your browser: http://127.0.0.1:8521/
 * In your browser hit *run*
diff --git a/examples/boltzmann_wealth_model_network/README.md b/examples/boltzmann_wealth_model_network/README.md
index d3c41c59..8a33d096 100644
--- a/examples/boltzmann_wealth_model_network/README.md
+++ b/examples/boltzmann_wealth_model_network/README.md
@@ -22,10 +22,10 @@ To install the dependencies use pip and the requirements.txt in this directory.
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
diff --git a/examples/color_patches/Readme.md b/examples/color_patches/Readme.md
index 41ac2f52..b2bd28ba 100644
--- a/examples/color_patches/Readme.md
+++ b/examples/color_patches/Readme.md
@@ -18,10 +18,10 @@ An agent's state represents its "opinion" and is shown by the color of the cell
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ``` 
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. 
diff --git a/examples/conways_game_of_life/Readme.md b/examples/conways_game_of_life/Readme.md
index 825f7ae2..301e5882 100644
--- a/examples/conways_game_of_life/Readme.md
+++ b/examples/conways_game_of_life/Readme.md
@@ -9,10 +9,10 @@ The "game" is a zero-player game, meaning that its evolution is determined by it
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ``` 
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press ``run``. 
diff --git a/examples/forest_fire/readme.md b/examples/forest_fire/readme.md
index fc54c209..3e62cabd 100644
--- a/examples/forest_fire/readme.md
+++ b/examples/forest_fire/readme.md
@@ -6,10 +6,10 @@ The [forest fire model](http://en.wikipedia.org/wiki/Forest-fire_model) is a sim
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. 
diff --git a/examples/hex_snowflake/Readme.md b/examples/hex_snowflake/Readme.md
index 94b8a668..96ad42ee 100644
--- a/examples/hex_snowflake/Readme.md
+++ b/examples/hex_snowflake/Readme.md
@@ -7,10 +7,10 @@ In this model, each dead cell will become alive if it has exactly one neighbor.
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press ``run``.
diff --git a/examples/pd_grid/readme.md b/examples/pd_grid/readme.md
index 7b5ba8af..83b9fb59 100644
--- a/examples/pd_grid/readme.md
+++ b/examples/pd_grid/readme.md
@@ -19,7 +19,7 @@ The Demographic Prisoner's Dilemma demonstrates how simple rules can lead to the
 
 ##### Web based model simulation
 
-To run the model interactively, run ``mesa run`` in this directory.
+To run the model interactively, run ``mesa runserver`` in this directory.
 
 ##### Jupyter Notebook
 
diff --git a/examples/shape_example/Readme.md b/examples/shape_example/Readme.md
index dcf2d15f..7552d33e 100644
--- a/examples/shape_example/Readme.md
+++ b/examples/shape_example/Readme.md
@@ -25,10 +25,10 @@ e.g.
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and
diff --git a/examples/sugarscape_cg/Readme.md b/examples/sugarscape_cg/Readme.md
index e187d46b..41772a9d 100644
--- a/examples/sugarscape_cg/Readme.md
+++ b/examples/sugarscape_cg/Readme.md
@@ -33,10 +33,10 @@ To install the dependencies use pip and the requirements.txt in this directory.
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
diff --git a/examples/virus_on_network/README.md b/examples/virus_on_network/README.md
index 2b02ca74..3f1b6494 100644
--- a/examples/virus_on_network/README.md
+++ b/examples/virus_on_network/README.md
@@ -18,10 +18,10 @@ To install the dependencies use pip and the requirements.txt in this directory.
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
diff --git a/examples/wolf_sheep/Readme.md b/examples/wolf_sheep/Readme.md
index 05c249d6..ae4ffbb6 100644
--- a/examples/wolf_sheep/Readme.md
+++ b/examples/wolf_sheep/Readme.md
@@ -24,10 +24,10 @@ To install the dependencies use pip and the requirements.txt in this directory.
 
 ## How to Run
 
-To run the model interactively, run ``mesa run`` in this directory. e.g.
+To run the model interactively, run ``mesa runserver`` in this directory. e.g.
 
 ```
-    $ mesa run
+    $ mesa runserver
 ```
 
 Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py
index 311fc1f6..f900f9af 100644
--- a/mesa/batchrunner.py
+++ b/mesa/batchrunner.py
@@ -182,7 +182,7 @@ class BatchRunner:
         for agent in model.schedule._agents.values():
             agent_record = {}
             for var, reporter in self.agent_reporters.items():
-                agent_record[var] = getattr(agent, reporter)
+                agent_record[var] = reporter(agent)
             agent_vars[agent.unique_id] = agent_record
         return agent_vars
 
diff --git a/mesa/cookiecutter-mesa/cookiecutter.json b/mesa/cookiecutter-mesa/cookiecutter.json
new file mode 100644
index 00000000..91e05326
--- /dev/null
+++ b/mesa/cookiecutter-mesa/cookiecutter.json
@@ -0,0 +1,8 @@
+{
+    "project": "Example Project",
+    "snake": "{{ cookiecutter.project.lower().replace(' ', '_') }}",
+    "camel": "{{ cookiecutter.project.title().replace(' ', '') }}",
+    "agent": "{{ cookiecutter.camel + 'Agent'}}",
+    "model": "{{ cookiecutter.camel + 'Model'}}",
+    "description": "Example short description of the project"
+}
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md
new file mode 100644
index 00000000..c96aa5c3
--- /dev/null
+++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md
@@ -0,0 +1,4 @@
+{{cookiecutter.project}}
+========================
+
+{{cookiecutter.description}}
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.py
new file mode 100644
index 00000000..66f5cea5
--- /dev/null
+++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.py
@@ -0,0 +1,3 @@
+from {{cookiecutter.snake}}.server import server  # noqa
+
+server.launch()
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py
new file mode 100644
index 00000000..69421ad6
--- /dev/null
+++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from setuptools import setup, find_packages
+
+requires = [
+    'mesa'
+]
+
+setup(
+    name='{{cookiecutter.snake}}',
+    version='0.0.1',
+    packages=find_packages(),
+    install_requires=requires
+)
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.py
new file mode 100644
index 00000000..0de34c1e
--- /dev/null
+++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.py
@@ -0,0 +1,65 @@
+import random
+
+from mesa import Agent, Model
+from mesa.time import RandomActivation
+from mesa.space import MultiGrid
+from mesa.datacollection import DataCollector
+
+
+class {{cookiecutter.agent}}(Agent):  # noqa
+    """
+    An agent
+    """
+
+    def __init__(self, unique_id, model):
+        """
+        Customize the agent
+        """
+        self.unique_id = unique_id
+        super().__init__(unique_id, model)
+
+    def step(self):
+        """
+        Modify this method to change what an individual agent will do during each step.
+        Can include logic based on neighbors states.
+        """
+        pass
+
+
+class {{cookiecutter.model}}(Model):
+    """
+    The model class holds the model-level attributes, manages the agents, and generally handles
+    the global level of our model.
+
+    There is only one model-level parameter: how many agents the model contains. When a new model
+    is started, we want it to populate itself with the given number of agents.
+
+    The scheduler is a special model component which controls the order in which agents are activated.
+    """
+
+    def __init__(self, num_agents, width, height):
+        super().__init__()
+        self.num_agents = num_agents
+        self.schedule = RandomActivation(self)
+        self.grid = MultiGrid(width=width, height=height, torus=True)
+
+        for i in range(self.num_agents):
+            agent = {{cookiecutter.agent}}(i, self)
+            self.schedule.add(agent)
+
+            x = random.randrange(self.grid.width)
+            y = random.randrange(self.grid.height)
+            self.grid.place_agent(agent, (x, y))
+
+        # example data collector
+        self.datacollector = DataCollector()
+
+        self.running = True
+        self.datacollector.collect(self)
+
+    def step(self):
+        """
+        A model step. Used for collecting data and advancing the schedule
+        """
+        self.datacollector.collect(self)
+        self.schedule.step()
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.py
new file mode 100644
index 00000000..fa28ecb7
--- /dev/null
+++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.py
@@ -0,0 +1,31 @@
+"""
+Configure visualization elements and instantiate a server
+"""
+
+from .model import {{ cookiecutter.model }}, {{ cookiecutter.agent }}  # noqa
+
+from mesa.visualization.ModularVisualization import ModularServer
+from mesa.visualization.modules import CanvasGrid, ChartModule
+
+
+def circle_portrayal_example(agent):
+    if agent is None:
+        return
+
+    portrayal = {"Shape": "circle",
+                 "Filled": "true",
+                 "Layer": 0,
+                 "r": 0.5,
+                 "Color": "Pink"}
+    return portrayal
+
+
+canvas_element = CanvasGrid(circle_portrayal_example, 20, 20, 500, 500)
+chart_element = ChartModule([{"Label": "{{ cookiecutter.camel }}", "Color": "Pink"}])
+
+model_kwargs = {"num_agents": 10,
+                "width": 10,
+                "height": 10}
+
+server = ModularServer({{ cookiecutter.model }}, [canvas_element, chart_element],  # noqa
+                       "{{ cookiecutter.camel }}", model_kwargs)
diff --git a/mesa/main.py b/mesa/main.py
index c9a7d2eb..f7ee6cba 100644
--- a/mesa/main.py
+++ b/mesa/main.py
@@ -1,18 +1,24 @@
 import sys
 import os
 import click
+from subprocess import call
 
 PROJECT_PATH = click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
+COOKIECUTTER_DIR = 'mesa/cookiecutter-mesa'
+SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
+COOKIECUTTER_PATH = os.path.join(os.path.dirname(SCRIPTS_DIR),
+                                 COOKIECUTTER_DIR)
 
 
 @click.group()
 def cli():
     'Manage Mesa projects'
+    pass
 
 
 @cli.command()
 @click.argument('project', type=PROJECT_PATH, default='.')
-def run(project):
+def runserver(project):
     '''Run mesa project PROJECT
 
     PROJECT is the path to the directory containing `run.py`, or the current
@@ -24,3 +30,17 @@ def run(project):
     with open("run.py") as f:
         code = compile(f.read(), "run.py", 'exec')
         exec(code, {}, {})
+
+
+@click.command()
+@click.option('--no-input', is_flag=True,
+              help='Do not prompt user for custom mesa model input.')
+def startproject(no_input):
+    args = ['cookiecutter', COOKIECUTTER_PATH]
+    if no_input:
+        args.append('--no-input')
+    call(args)
+
+
+cli.add_command(runserver)
+cli.add_command(startproject)
diff --git a/mesa/visualization/modules/ChartVisualization.py b/mesa/visualization/modules/ChartVisualization.py
index 069ff99f..ed5b740b 100644
--- a/mesa/visualization/modules/ChartVisualization.py
+++ b/mesa/visualization/modules/ChartVisualization.py
@@ -74,6 +74,9 @@ class ChartModule(VisualizationElement):
 
         for s in self.series:
             name = s["Label"]
-            val = data_collector.model_vars[name][-1]  # Latest value
+            try:
+                val = data_collector.model_vars[name][-1]  # Latest value
+            except (IndexError, KeyError) as e:
+                val = 0
             current_values.append(val)
         return current_values
diff --git a/setup.py b/setup.py
index 9e6bb893..c9f8d892 100644
--- a/setup.py
+++ b/setup.py
@@ -6,19 +6,20 @@ from setuptools import setup, find_packages
 from codecs import open
 
 requires = [
-    'tornado',
+    'click',
+    'cookiecutter',
+    'jupyter',
     'networkx',
     'numpy',
     'pandas',
-    'jupyter',
+    'tornado >= 4.2, < 5.0.0',
     'tqdm',
-    'click',
 ]
 
 extras_require = {
     'dev': [
-        'flake8',
         'coverage',
+        'flake8',
         'nose',
         'sphinx',
     ],
@@ -45,7 +46,8 @@ setup(
     url='https://github.com/projectmesa/mesa',
     packages=find_packages(),
     package_data={'mesa': ['visualization/templates/*.html', 'visualization/templates/css/*',
-                           'visualization/templates/fonts/*', 'visualization/templates/js/*']},
+                           'visualization/templates/fonts/*', 'visualization/templates/js/*'],
+                  'cookiecutter-mesa': ['cookiecutter-mesa/*']},
     include_package_data=True,
     install_requires=requires,
     extras_require=extras_require,


Test patch:
diff --git a/tests/test_batchrunner.py b/tests/test_batchrunner.py
index 21befb9e..f08645a4 100644
--- a/tests/test_batchrunner.py
+++ b/tests/test_batchrunner.py
@@ -74,8 +74,8 @@ class TestBatchRunner(unittest.TestCase):
             "reported_fixed_value": lambda m: m.fixed_model_param
         }
         self.agent_reporters = {
-            "agent_id": "unique_id",
-            "agent_val": "val"
+            "agent_id": lambda a: a.unique_id,
+            "agent_val": lambda a: a.val
         }
         self.variable_params = {
             "variable_model_param": range(3),
diff --git a/tests/test_main.py b/tests/test_main.py
index 7bc76fa9..b0fdeed6 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -23,6 +23,6 @@ class TestCli(unittest.TestCase):
         with patch('mesa.visualization.ModularVisualization.ModularServer') as ModularServer:
             example_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../examples/wolf_sheep'))
             with self.runner.isolated_filesystem():
-                result = self.runner.invoke(cli, ['run', example_dir])
+                result = self.runner.invoke(cli, ['runserver', example_dir])
                 assert result.exit_code == 0, result.output
                 assert ModularServer().launch.call_count == 1
diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py
new file mode 100644
index 00000000..f9b62b22
--- /dev/null
+++ b/tests/test_scaffold.py
@@ -0,0 +1,21 @@
+import unittest
+import os
+from click.testing import CliRunner
+
+from mesa.main import cli
+
+
+class ScaffoldTest(unittest.TestCase):
+    """
+    Test mesa project scaffolding command
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        cls.runner = CliRunner()
+
+    def test_scaffold_creates_project_dir(self):
+        with self.runner.isolated_filesystem():
+            assert not os.path.isdir("example_project")
+            self.runner.invoke(cli, ['startproject', '--no-input'])
+            assert os.path.isdir("example_project")