Skip to content

Commit a324989

Browse files
authored
Merge pull request #492 from rstudio/new-app-entrypoints
Infer entrypoints from additional filename patterns
2 parents f4def0c + 0243728 commit a324989

File tree

4 files changed

+55
-39
lines changed

4 files changed

+55
-39
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
hosts that should not be accessed via proxy server. It's a comma-separated list
1515
of host or domain suffixes. For example, specifying `example.com` will
1616
bypass the proxy for example.com, host.example.com, etc.
17+
- If an entrypoint is not specified with `--entrypoint`, rsconnect-python will try
18+
harder than before to choose an entrypoint file. In addition to the previously
19+
recognized filename patterns, the file patterns `app-*.py`, `app_*.py`, `*-app.py`,
20+
and `*_app.py` are now considered. However, if the directory contains more than
21+
one file matching these new patterns, you must provide rsconnect-python with an
22+
explicit `--entrypoint` argument.
1723

1824
## [1.20.0] - 2023-09-11
1925

rsconnect/actions.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from os.path import abspath, basename, dirname, exists, isdir, join, relpath, splitext
1717
from .exception import RSConnectException
1818
from . import api
19+
from . import bundle
1920
from .bundle import (
2021
_warn_if_environment_directory,
2122
_warn_if_no_requirements_file,
@@ -415,21 +416,7 @@ def validate_manifest_file(file_or_directory):
415416

416417
def get_default_entrypoint(directory):
417418
warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2)
418-
candidates = ["app", "application", "main", "api"]
419-
files = set(os.listdir(directory))
420-
421-
for candidate in candidates:
422-
filename = candidate + ".py"
423-
if filename in files:
424-
return candidate
425-
426-
# if only one python source file, use it
427-
python_files = list(filter(lambda s: s.endswith(".py"), files))
428-
if len(python_files) == 1:
429-
return python_files[0][:-3]
430-
431-
logger.warning("Can't determine entrypoint; defaulting to 'app'")
432-
return "app"
419+
return bundle.get_default_entrypoint(directory)
433420

434421

435422
def validate_entry_point(entry_point, directory):

rsconnect/bundle.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,10 @@ def validate_manifest_file(file_or_directory):
14041404
return file_or_directory
14051405

14061406

1407+
re_app_prefix = re.compile(r"^app[-_].+\.py$")
1408+
re_app_suffix = re.compile(r".+[-_]app\.py$")
1409+
1410+
14071411
def get_default_entrypoint(directory):
14081412
candidates = ["app", "application", "main", "api"]
14091413
files = set(os.listdir(directory))
@@ -1418,18 +1422,24 @@ def get_default_entrypoint(directory):
14181422
if len(python_files) == 1:
14191423
return python_files[0][:-3]
14201424

1421-
logger.warning("Can't determine entrypoint; defaulting to 'app'")
1422-
return "app"
1425+
# try app-*.py, app_*.py, *-app.py, *_app.py
1426+
app_files = list(filter(lambda s: re_app_prefix.match(s) or re_app_suffix.match(s), python_files))
1427+
if len(app_files) == 1:
1428+
# In these cases, the app should be in the "app" attribute
1429+
return app_files[0][:-3]
1430+
1431+
raise RSConnectException(f"Could not determine default entrypoint file in directory '{directory}'")
14231432

14241433

14251434
def validate_entry_point(entry_point, directory):
14261435
"""
14271436
Validates the entry point specified by the user, expanding as necessary. If the
14281437
user specifies nothing, a module of "app" is assumed. If the user specifies a
1429-
module only, the object is assumed to be the same name as the module.
1438+
module only, at runtime the following object names will be tried in order: `app`,
1439+
`application`, `create_app`, and `make_app`.
14301440
14311441
:param entry_point: the entry point as specified by the user.
1432-
:return: the fully expanded and validated entry point and the module file name..
1442+
:return: An entry point, in the form of "module" or "module:app".
14331443
"""
14341444
if not entry_point:
14351445
entry_point = get_default_entrypoint(directory)

tests/test_bundle.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import json
33
import os
44
import pytest
5-
import shutil
65
import subprocess
76
import sys
87
import tarfile
98
import tempfile
9+
from pathlib import Path
1010

1111
from os.path import dirname, join, basename, abspath
1212
from unittest import mock, TestCase
@@ -1081,25 +1081,38 @@ def test_validate_title(self):
10811081
_validate_title("1" * 1024)
10821082

10831083
def test_validate_entry_point(self):
1084-
directory = tempfile.mkdtemp()
1085-
1086-
try:
1087-
self.assertEqual(validate_entry_point(None, directory), "app")
1088-
self.assertEqual(validate_entry_point("app", directory), "app")
1089-
self.assertEqual(validate_entry_point("app:app", directory), "app:app")
1090-
1091-
with self.assertRaises(RSConnectException):
1092-
validate_entry_point("x:y:z", directory)
1093-
1094-
with open(join(directory, "onlysource.py"), "w") as f:
1095-
f.close()
1096-
self.assertEqual(validate_entry_point(None, directory), "onlysource")
1097-
1098-
with open(join(directory, "main.py"), "w") as f:
1099-
f.close()
1100-
self.assertEqual(validate_entry_point(None, directory), "main")
1101-
finally:
1102-
shutil.rmtree(directory)
1084+
# Simple cases
1085+
for case in ["app", "application", "main", "api", "app-example", "app_example", "example-app", "example_app"]:
1086+
self._entry_point_case(["helper.py", f"{case}.py"], None, case)
1087+
1088+
# only one Python file means we assume it's the entrypoint
1089+
self._entry_point_case(["onlysource.py"], None, "onlysource")
1090+
1091+
# Explicit entrypoint specifiers, no need to infer
1092+
self._entry_point_case(["helper.py", "app.py"], "app", "app")
1093+
self._entry_point_case(["helper.py", "app.py"], "app:app", "app:app")
1094+
self._entry_point_case(["helper.py", "app.py"], "foo:bar", "foo:bar")
1095+
1096+
def test_validate_entry_point_failure(self):
1097+
# Invalid entrypoint specifier
1098+
self._entry_point_case(["app.py"], "x:y:z", False)
1099+
# Nothing relevant found
1100+
self._entry_point_case(["one.py", "two.py"], "x:y:z", False)
1101+
# Too many app-*.py files
1102+
self._entry_point_case(["app-one.py", "app-two.py"], "x:y:z", False)
1103+
1104+
def _entry_point_case(self, files, entry_point, expected):
1105+
with tempfile.TemporaryDirectory() as directory:
1106+
dir = Path(directory)
1107+
1108+
for file in files:
1109+
(dir / file).touch()
1110+
1111+
if expected is False:
1112+
with self.assertRaises(RSConnectException):
1113+
validate_entry_point(entry_point, directory)
1114+
else:
1115+
self.assertEqual(validate_entry_point(entry_point, directory), expected)
11031116

11041117
def test_default_title(self):
11051118
self.assertEqual(_default_title("testing.txt"), "testing")

0 commit comments

Comments
 (0)