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

pgzrun a directory #332

Merged
merged 1 commit into from
Jan 18, 2025
Merged
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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
write_to = "src/pgzero/_version.py"

[tool.ruff]
builtins = [
"Actor", "Rect", "ZRect", "animate", "clock", "exit", "images", "keyboard", "keymods",
"keys", "mouse", "music", "screen", "sounds", "storage", "tone"
]
61 changes: 53 additions & 8 deletions src/pgzero/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,30 +69,75 @@ def main():
help="Print periodic FPS measurements on the terminal."
)
parser.add_argument(
'program',
help="The Pygame Zero program to run."
'game',
help="The Pygame Zero game to run (a Python file or directory)."
)
args = parser.parse_args()

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

load_and_run(args.program, fps=args.fps)
try:
load_and_run(args.game, fps=args.fps)
except NoMainModule as e:
sys.exit(e)


class NoMainModule(Exception):
"""Indicate that we couldn't find a main module to run."""


def load_and_run(path, *, fps: bool = False):
"""Load and run the given Python file as the main PGZero game module.
"""Load and run the given Python file or directory.

If a file, run this as the main PGZero game module.

If a directory, run the first file inside the directory containing:

* `__main__.py`
* `main.py`
* `run_game.py`
* `<name>.py` where `<name>` is the basename of the directory

Note that the 'import pgzrun' IDE mode doesn't pass through this entry
point, as the module is already loaded.

"""
with open(path, 'rb') as f:
src = f.read()
path = path.rstrip(os.sep)
try:
with open(path, 'rb') as f:
src = f.read()
except FileNotFoundError:
raise NoMainModule(f"Error: {path} does not exist.")
except IsADirectoryError:
name = os.path.basename(path)
for candidate in (
'__main__.py',
'main.py',
'run_game.py',
f'{name}.py'
):
try:
with open(os.path.join(path, candidate), 'rb') as f:
src = f.read()
break
except FileNotFoundError:
pass
else:
raise NoMainModule(f"""\
Error: {path} is a directory.

To run a directory with pgzrun, it must contain a file named __main__.py,
main.py, run_game.py, or a .py file with the same name as the directory.

You can also run a specific file with:

pgzrun path/to/your/file.py
""")
else:
name, _ = os.path.splitext(os.path.basename(path))

code = compile(src, os.path.basename(path), 'exec', dont_inherit=True)

name, _ = os.path.splitext(os.path.basename(path))
mod = ModuleType(name)
mod.__file__ = path
mod.__name__ = name
Expand Down
2 changes: 2 additions & 0 deletions test/game_tests/blue/run_game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('blue')
2 changes: 2 additions & 0 deletions test/game_tests/green/green.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('green')
2 changes: 2 additions & 0 deletions test/game_tests/pink/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('pink')
2 changes: 2 additions & 0 deletions test/game_tests/red/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('red')
30 changes: 24 additions & 6 deletions test/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,32 @@
class RunnerTest(unittest.TestCase):
"""Test that we can load and run the current file."""

def test_run(self):
"""We can load and run a game saved as UTF-8."""
def assert_runnable(self, path: Path):
"""Check that we can run the given file."""
clock.schedule_unique(sys.exit, 0.05)
with self.assertRaises(SystemExit):
load_and_run(str(game_tests / 'utf8.py'))
load_and_run(str(path))

def test_run_utf8(self):
"""We can load and run a game saved as UTF-8."""
self.assert_runnable(game_tests / 'utf8.py')

def test_import(self):
"""Games can import other modules, which can acccess the builtins."""
clock.schedule_unique(sys.exit, 0.05)
with self.assertRaises(SystemExit):
load_and_run(str(game_tests / 'importing.py'))
self.assert_runnable(game_tests / 'importing.py')

def test_run_directory_dunder_main(self):
"""We can run a directory containing __main__.py"""
self.assert_runnable(game_tests / 'red')

def test_run_directory_main(self):
"""We can run a directory containing main.py"""
self.assert_runnable(game_tests / 'pink')

def test_run_directory_ane_name(self):
"""We can run a directory containing <basename>.py"""
self.assert_runnable(game_tests / 'green')

def test_run_directory_run_game(self):
"""We can run a directory containing run_game.py"""
self.assert_runnable(game_tests / 'blue')
Loading