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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,6 +96,8 @@ Unreleased changes template.
([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
which allows pass arguments to the interpreter before the regular args.
* (rules) Added {obj}`main_module` attribute to `py_binary` and `py_test`,
which allows specifying a module name to run (i.e. `python -m <module>`).
Comment thread
rickeylev marked this conversation as resolved.

{#v0-0-0-removed}
### Removed
Expand Down
36 changes: 34 additions & 2 deletions python/private/py_executable.bzl
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,6 +126,24 @@ Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.

This is mutually exclusive with {obj}`main_module`.
""",
),
"main_module": lambda: attrb.String(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about main becoming main_module if it cannot find the file in srcs?

That said, then we would have worse error messages, so this solution is also good.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not because it's not clear how to transform the file name into a module name.

doc = """
Module name to execute as the main program.

When set, `srcs` is not required, and it is assumed the module is
provided by a dependency.

See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more
information about running modules as the main program.

This is mutually exclusive with {obj}`main`.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"pyc_collection": lambda: attrb.String(
Expand DownExpand Up@@ -638,14 +656,19 @@ def _create_stage2_bootstrap(

template = runtime.stage2_bootstrap_template

if main_py:
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
else:
main_py_path = ""
ctx.actions.expand_template(
template = template,
output = output,
substitutions = {
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
"%main%": main_py_path,
"%main_module%": ctx.attr.main_module,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
},
Expand DownExpand Up@@ -929,7 +952,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
"""
_validate_executable(ctx)

main_py = determine_main(ctx)
if not ctx.attr.main_module:
main_py = determine_main(ctx)
else:
main_py = None
direct_sources = filter_to_py_srcs(ctx.files.srcs)
precompile_result = semantics.maybe_precompile(ctx, direct_sources)

Expand DownExpand Up@@ -1049,6 +1075,12 @@ def _validate_executable(ctx):
if ctx.attr.python_version == "PY2":
fail("It is not allowed to use Python 2")

if ctx.attr.main and ctx.attr.main_module:
fail((
"Only one of main and main_module can be set, got: " +
"main={}, main_module={}"
).format(ctx.attr.main, ctx.attr.main_module))

def _declare_executable_file(ctx):
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
Expand Down
114 changes: 69 additions & 45 deletions python/private/stage2_bootstrap_template.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,11 @@
# We just put them in one place so its easy to tell which are used.

# Runfiles-relative path to the main Python source file.
MAIN = "%main%"
# Empty if MAIN_MODULE is used
MAIN_PATH = "%main%"

# Module name to execute. Empty if MAIN is used.
MAIN_MODULE = "%main_module%"

# ===== Template substitutions end =====

Expand DownExpand Up@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
os.unlink(unfixed_file)


def _run_py(main_filename, *, args, cwd=None):
def _run_py_path(main_filename, *, args, cwd=None):
# type: (str, str, list[str], dict[str, str]) -> ...
"""Executes the given Python file using the various environment settings."""

Expand All@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
sys.argv = orig_argv


def _run_py_module(module_name):
# Match `python -m` behavior, so modify sys.argv and the run name
runpy.run_module(module_name, alter_sys=True, run_name="__main__")


@contextlib.contextmanager
def _maybe_collect_coverage(enable):
print_verbose_coverage("enabled:", enable)
Expand DownExpand Up@@ -356,64 +365,79 @@ def main():
print_verbose("initial environ:", mapping=os.environ)
print_verbose("initial sys.path:", values=sys.path)

main_rel_path = MAIN
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

module_space = find_runfiles_root(main_rel_path)
print_verbose("runfiles root:", module_space)

# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(module_space, os.path.dirname(main_rel_path))
]
main_rel_path = None
# todo: things happen to work because find_runfiles_root
# ends up using stage2_bootstrap, and ends up computing the proper
# runfiles root
if MAIN_PATH:
main_rel_path = MAIN_PATH
if is_windows():
main_rel_path = main_rel_path.replace("/", os.sep)

runfiles_root = find_runfiles_root(main_rel_path)
else:
prepend_path_entries = []
runfiles_root = find_runfiles_root("")

print_verbose("runfiles root:", runfiles_root)

runfiles_envkey, runfiles_envvalue = runfiles_envvar(module_space)
runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root)
if runfiles_envkey:
os.environ[runfiles_envkey] = runfiles_envvalue

main_filename = os.path.join(module_space, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)
if MAIN_PATH:
# Recreate the "add main's dir to sys.path[0]" behavior to match the
# system-python bootstrap / typical Python behavior.
#
# Without safe path enabled, when `python foo/bar.py` is run, python will
# resolve the foo/bar.py symlink to its real path, then add the directory
# of that path to sys.path. But, the resolved directory for the symlink
# depends on if the file is generated or not.
#
# When foo/bar.py is a source file, then it's a symlink pointing
# back to the client source directory. This means anything from that source
# directory becomes importable, i.e. most code is importable.
#
# When foo/bar.py is a generated file, then it's a symlink pointing to
# somewhere under bazel-out/.../bin, i.e. where generated files are. This
# means only other generated files are importable (not source files).
#
# To replicate this behavior, we add main's directory within the runfiles
# when safe path isn't enabled.
if not getattr(sys.flags, "safe_path", False):
prepend_path_entries = [
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
]
else:
prepend_path_entries = []

main_filename = os.path.join(runfiles_root, main_rel_path)
main_filename = get_windows_path_with_unc_prefix(main_filename)
assert os.path.exists(main_filename), (
"Cannot exec() %r: file not found." % main_filename
)
assert os.access(main_filename, os.R_OK), (
"Cannot exec() %r: file not readable." % main_filename
)

sys.stdout.flush()
sys.stdout.flush()

sys.path[0:0] = prepend_path_entries
sys.path[0:0] = prepend_path_entries
else:
main_filename = None

if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

coverage_enabled = _bazel_site_init.COVERAGE_SETUP
else:
coverage_enabled = False

with _maybe_collect_coverage(enable=coverage_enabled):
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py(main_filename, args=sys.argv[1:])
if MAIN_PATH:
# The first arg is this bootstrap, so drop that for the re-invocation.
_run_py_path(main_filename, args=sys.argv[1:])
else:
_run_py_module(MAIN_MODULE)
sys.exit(0)


Expand Down
9 changes: 9 additions & 0 deletions tests/bootstrap_impls/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,15 @@ py_reconfig_test(
main = "sys_path_order_test.py",
)

py_reconfig_test(
name = "main_module_test",
srcs = ["main_module.py"],
bootstrap_impl = "script",
imports = ["."],
main_module = "tests.bootstrap_impls.main_module",
target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
)

sh_py_run_test(
name = "inherit_pythonsafepath_env_test",
bootstrap_impl = "script",
Expand Down
17 changes: 17 additions & 0 deletions tests/bootstrap_impls/main_module.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import sys
import unittest


class MainModuleTest(unittest.TestCase):
def test_run_as_module(self):
self.assertIsNotNone(__spec__, "__spec__ was none")
# If not run as a module, __spec__ is None
self.assertNotEqual(__name__, __spec__.name)
self.assertEqual(__spec__.name, "tests.bootstrap_impls.main_module")


if __name__ == "__main__":
unittest.main()
else:
# Guard against running it as a module in a non-main way.
sys.exit(f"__name__ should be __main__, got {__name__}")