Skip to content

Commit 29d0361

Browse files
committed
pgzrun a directory
Fixes #330
1 parent bde0fbe commit 29d0361

File tree

7 files changed

+91
-14
lines changed

7 files changed

+91
-14
lines changed

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ build-backend = "setuptools.build_meta"
2323

2424
[tool.setuptools_scm]
2525
write_to = "src/pgzero/_version.py"
26+
27+
[tool.ruff]
28+
builtins = [
29+
"Actor", "Rect", "ZRect", "animate", "clock", "exit", "images", "keyboard", "keymods",
30+
"keys", "mouse", "music", "screen", "sounds", "storage", "tone"
31+
]

src/pgzero/runner.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,75 @@ def main():
6969
help="Print periodic FPS measurements on the terminal."
7070
)
7171
parser.add_argument(
72-
'program',
73-
help="The Pygame Zero program to run."
72+
'game',
73+
help="The Pygame Zero game to run (a Python file or directory)."
7474
)
7575
args = parser.parse_args()
7676

7777
if __debug__:
7878
warnings.simplefilter('default', DeprecationWarning)
7979

80-
load_and_run(args.program, fps=args.fps)
80+
try:
81+
load_and_run(args.game, fps=args.fps)
82+
except NoMainModule as e:
83+
sys.exit(e)
84+
85+
86+
class NoMainModule(Exception):
87+
"""Indicate that we couldn't find a main module to run."""
8188

8289

8390
def load_and_run(path, *, fps: bool = False):
84-
"""Load and run the given Python file as the main PGZero game module.
91+
"""Load and run the given Python file or directory.
92+
93+
If a file, run this as the main PGZero game module.
94+
95+
If a directory, run the first file inside the directory containing:
96+
97+
* `__main__.py`
98+
* `main.py`
99+
* `run_game.py`
100+
* `<name>.py` where `<name>` is the basename of the directory
85101
86102
Note that the 'import pgzrun' IDE mode doesn't pass through this entry
87103
point, as the module is already loaded.
88104
89105
"""
90-
with open(path, 'rb') as f:
91-
src = f.read()
106+
path = path.rstrip(os.sep)
107+
try:
108+
with open(path, 'rb') as f:
109+
src = f.read()
110+
except FileNotFoundError:
111+
raise NoMainModule(f"Error: {path} does not exist.")
112+
except IsADirectoryError:
113+
name = os.path.basename(path)
114+
for candidate in (
115+
'__main__.py',
116+
'main.py',
117+
'run_game.py',
118+
f'{name}.py'
119+
):
120+
try:
121+
with open(os.path.join(path, candidate), 'rb') as f:
122+
src = f.read()
123+
break
124+
except FileNotFoundError:
125+
pass
126+
else:
127+
raise NoMainModule(f"""\
128+
Error: {path} is a directory.
129+
130+
To run a directory with pgzrun, it must contain a file named __main__.py,
131+
main.py, run_game.py, or a .py file with the same name as the directory.
132+
133+
You can also run a specific file with:
134+
135+
pgzrun path/to/your/file.py
136+
""")
137+
else:
138+
name, _ = os.path.splitext(os.path.basename(path))
92139

93140
code = compile(src, os.path.basename(path), 'exec', dont_inherit=True)
94-
95-
name, _ = os.path.splitext(os.path.basename(path))
96141
mod = ModuleType(name)
97142
mod.__file__ = path
98143
mod.__name__ = name

test/game_tests/blue/run_game.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def draw():
2+
screen.fill('blue')

test/game_tests/green/green.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def draw():
2+
screen.fill('green')

test/game_tests/pink/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def draw():
2+
screen.fill('pink')

test/game_tests/red/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def draw():
2+
screen.fill('red')

test/test_runner.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,32 @@
1616
class RunnerTest(unittest.TestCase):
1717
"""Test that we can load and run the current file."""
1818

19-
def test_run(self):
20-
"""We can load and run a game saved as UTF-8."""
19+
def assert_runnable(self, path: Path):
20+
"""Check that we can run the given file."""
2121
clock.schedule_unique(sys.exit, 0.05)
2222
with self.assertRaises(SystemExit):
23-
load_and_run(str(game_tests / 'utf8.py'))
23+
load_and_run(str(path))
24+
25+
def test_run_utf8(self):
26+
"""We can load and run a game saved as UTF-8."""
27+
self.assert_runnable(game_tests / 'utf8.py')
2428

2529
def test_import(self):
2630
"""Games can import other modules, which can acccess the builtins."""
27-
clock.schedule_unique(sys.exit, 0.05)
28-
with self.assertRaises(SystemExit):
29-
load_and_run(str(game_tests / 'importing.py'))
31+
self.assert_runnable(game_tests / 'importing.py')
32+
33+
def test_run_directory_dunder_main(self):
34+
"""We can run a directory containing __main__.py"""
35+
self.assert_runnable(game_tests / 'red')
36+
37+
def test_run_directory_main(self):
38+
"""We can run a directory containing main.py"""
39+
self.assert_runnable(game_tests / 'pink')
40+
41+
def test_run_directory_ane_name(self):
42+
"""We can run a directory containing <basename>.py"""
43+
self.assert_runnable(game_tests / 'green')
44+
45+
def test_run_directory_run_game(self):
46+
"""We can run a directory containing run_game.py"""
47+
self.assert_runnable(game_tests / 'blue')

0 commit comments

Comments
 (0)