From bcc6cab4f1846fa2580400996b9944dd52b73c91 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 8 Apr 2026 18:49:05 -0700 Subject: [PATCH 01/10] fix: namespace package on windows --- python/private/pypi/pypi_repo_utils.bzl | 5 ++-- python/private/repo_utils.bzl | 35 ++++++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/python/private/pypi/pypi_repo_utils.bzl b/python/private/pypi/pypi_repo_utils.bzl index d8e320014f..8ec7bd1dbe 100644 --- a/python/private/pypi/pypi_repo_utils.bzl +++ b/python/private/pypi/pypi_repo_utils.bzl @@ -177,7 +177,6 @@ def _find_namespace_package_files(rctx, install_dir): to namespace packages. """ - repo_root = str(rctx.path(".")) + "/" namespace_package_files = [] for top_level_dir in install_dir.readdir(): if not is_importable_name(top_level_dir.basename): @@ -192,7 +191,9 @@ def _find_namespace_package_files(rctx, install_dir): if ("__path__ =" in content and "pkgutil" in content and "extend_path(" in content): - namespace_package_files.append(str(init_py).removeprefix(repo_root)) + namespace_package_files.append( + repo_utils.repo_root_relative_path(rctx, init_py), + ) return namespace_package_files diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index a558fa08e1..10d60abfda 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -334,7 +334,7 @@ def _mkdir(mrctx, path): repo_root = str(mrctx.path(".")) path_str = str(path) - if not path_str.startswith(repo_root): + if not _is_relative_to(mrctx, path_str, repo_root): mkdir_bin = mrctx.which("mkdir") if not mkdir_bin: return None @@ -348,6 +348,28 @@ def _mkdir(mrctx, path): mrctx.delete(placeholder) return path +def _norm_path(mrctx, p): + p = str(p) + + # Windows is case-insensitive + if _get_platforms_os_name(mrctx) == "windows": + return p.lower() + return p + +def _relative_to(mrctx, path, parent, fail = fail): + path_d = _norm_path(mrctx, path) + "/" + parent_d = _norm_path(mrctx, parent) + "/" + if path_d.startswith(parent_d): + return path.removeprefix(parent_d) + else: + fail("{} is not relative to {}".format(path, parent)) + +def _is_relative_to(mrctx, path, parent): + """Tell if `path` is equal to or beneath `parent`.""" + path = _norm_path(mrctx, path) + parent = _norm_path(mrctx, parent) + return (parent + "/").startswith(path + "/") + def _repo_root_relative_path(mrctx, path): """Takes a path object and returns a repo-relative path string. @@ -360,14 +382,7 @@ def _repo_root_relative_path(mrctx, path): """ repo_root = str(mrctx.path(".")) path_str = str(path) - relative_path = path_str[len(repo_root):] - if relative_path[0] != "/": - fail("{path} not under {repo_root}".format( - path = path, - repo_root = repo_root, - )) - relative_path = relative_path[1:] - return relative_path + return _relative_to(mrctx, path_str, repo_root) def _args_to_str(arguments): return " ".join([_arg_repr(a) for a in arguments]) @@ -516,6 +531,8 @@ repo_utils = struct( is_repo_debug_enabled = _is_repo_debug_enabled, logger = _logger, mkdir = _mkdir, + norm_path = _norm_path, + relative_to = _relative_to, repo_root_relative_path = _repo_root_relative_path, which_checked = _which_checked, which_unchecked = _which_unchecked, From 5ec7baadbded2bc27c59875f841aef4b9e2e53e9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 01:58:03 -0700 Subject: [PATCH 02/10] fix relative path computation --- python/private/repo_utils.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index 10d60abfda..85ec87317b 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -360,7 +360,7 @@ def _relative_to(mrctx, path, parent, fail = fail): path_d = _norm_path(mrctx, path) + "/" parent_d = _norm_path(mrctx, parent) + "/" if path_d.startswith(parent_d): - return path.removeprefix(parent_d) + return path.removeprefix(parent + "/") else: fail("{} is not relative to {}".format(path, parent)) From 0fc2a9c2b5aa56ff0b2a39aa332f70ea2f039c6a Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 02:07:13 -0700 Subject: [PATCH 03/10] add test, fix mixed case --- python/private/repo_utils.bzl | 14 ++++---- tests/repo_utils/BUILD.bazel | 3 ++ tests/repo_utils/repo_utils_test.bzl | 53 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 tests/repo_utils/BUILD.bazel create mode 100644 tests/repo_utils/repo_utils_test.bzl diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index 85ec87317b..4159c46109 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -357,18 +357,20 @@ def _norm_path(mrctx, p): return p def _relative_to(mrctx, path, parent, fail = fail): - path_d = _norm_path(mrctx, path) + "/" - parent_d = _norm_path(mrctx, parent) + "/" + path_str = str(path) + parent_str = str(parent) + path_d = _norm_path(mrctx, path_str) + "/" + parent_d = _norm_path(mrctx, parent_str) + "/" if path_d.startswith(parent_d): - return path.removeprefix(parent + "/") + return path_str[len(parent_str):].removeprefix("/") else: fail("{} is not relative to {}".format(path, parent)) def _is_relative_to(mrctx, path, parent): """Tell if `path` is equal to or beneath `parent`.""" - path = _norm_path(mrctx, path) - parent = _norm_path(mrctx, parent) - return (parent + "/").startswith(path + "/") + path_d = _norm_path(mrctx, path) + "/" + parent_d = _norm_path(mrctx, parent) + "/" + return path_d.startswith(parent_d) def _repo_root_relative_path(mrctx, path): """Takes a path object and returns a repo-relative path string. diff --git a/tests/repo_utils/BUILD.bazel b/tests/repo_utils/BUILD.bazel new file mode 100644 index 0000000000..c1b1bfc304 --- /dev/null +++ b/tests/repo_utils/BUILD.bazel @@ -0,0 +1,3 @@ +load(":repo_utils_test.bzl", "repo_utils_test_suite") + +repo_utils_test_suite(name = "repo_utils_tests") diff --git a/tests/repo_utils/repo_utils_test.bzl b/tests/repo_utils/repo_utils_test.bzl new file mode 100644 index 0000000000..17480841d9 --- /dev/null +++ b/tests/repo_utils/repo_utils_test.bzl @@ -0,0 +1,53 @@ +"""Unit tests for repo_utils.bzl.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_get_platforms_os_name(env): + mock_mrctx = struct( + os = struct( + name = "Mac OS X", + ), + ) + got = repo_utils.get_platforms_os_name(mock_mrctx) + env.expect.that_str(got).equals("osx") + +_tests.append(_test_get_platforms_os_name) + +def _test_relative_to(env): + mock_mrctx_linux = struct(os = struct(name = "linux")) + mock_mrctx_win = struct(os = struct(name = "windows")) + + # Case-sensitive matching (Linux) + got = repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar") + env.expect.that_str(got).equals("baz") + + # Case-insensitive matching (Windows) + got = repo_utils.relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar") + env.expect.that_str(got).equals("Baz") + + # Failure case + failures = [] + def _mock_fail(msg): + failures.append(msg) + + repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "qux", fail = _mock_fail) + env.expect.that_collection(failures).contains_exactly(["foo/bar/baz is not relative to qux"]) + +_tests.append(_test_relative_to) + +def _test_is_relative_to(env): + # Note: `_is_relative_to` isn't publicly exported in `repo_utils` struct natively. + # Actually wait, `is_relative_to` is not exported by repo_utils.bzl! + # Let's skip testing `_is_relative_to` directly in this suite unless we export it or we don't need to test it directly. + pass + +def repo_utils_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) From a2d7abab79cc643516305d181065c68d731f70d5 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 02:13:47 -0700 Subject: [PATCH 04/10] refacator mocks to mocks --- tests/repo_utils/repo_utils_test.bzl | 11 ++++------ tests/support/mocks.bzl | 31 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 tests/support/mocks.bzl diff --git a/tests/repo_utils/repo_utils_test.bzl b/tests/repo_utils/repo_utils_test.bzl index 17480841d9..33ef298f0d 100644 --- a/tests/repo_utils/repo_utils_test.bzl +++ b/tests/repo_utils/repo_utils_test.bzl @@ -2,23 +2,20 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility +load("//tests/support:mocks.bzl", "mocks") _tests = [] def _test_get_platforms_os_name(env): - mock_mrctx = struct( - os = struct( - name = "Mac OS X", - ), - ) + mock_mrctx = mocks.rctx(os_name = "Mac OS X") got = repo_utils.get_platforms_os_name(mock_mrctx) env.expect.that_str(got).equals("osx") _tests.append(_test_get_platforms_os_name) def _test_relative_to(env): - mock_mrctx_linux = struct(os = struct(name = "linux")) - mock_mrctx_win = struct(os = struct(name = "windows")) + mock_mrctx_linux = mocks.rctx(os_name = "linux") + mock_mrctx_win = mocks.rctx(os_name = "windows") # Case-sensitive matching (Linux) got = repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar") diff --git a/tests/support/mocks.bzl b/tests/support/mocks.bzl new file mode 100644 index 0000000000..d6f6a59250 --- /dev/null +++ b/tests/support/mocks.bzl @@ -0,0 +1,31 @@ +"""Mocks for testing.""" + +def _rctx(os_name = "linux", os_arch = "x86_64", environ = None, **kwargs): + """Creates a mock of repository_ctx or module_ctx. + + Args: + os_name: The OS name to mock (e.g., "linux", "Mac OS X", "windows"). + os_arch: The OS architecture to mock (e.g., "x86_64", "aarch64"). + environ: A dictionary representing the environment variables. + **kwargs: Additional attributes to add to the mock struct. + + Returns: + A struct mocking repository_ctx. + """ + if environ == None: + environ = {} + + attrs = { + "os": struct( + name = os_name, + arch = os_arch, + ), + "getenv": environ.get, + } + attrs.update(kwargs) + + return struct(**attrs) + +mocks = struct( + rctx = _rctx, +) From 20f60247cc72e3f42ff308a35b4d2a7a6b18b2e7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 02:23:43 -0700 Subject: [PATCH 05/10] add more tests --- python/private/repo_utils.bzl | 1 + tests/repo_utils/repo_utils_test.bzl | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index 4159c46109..7ec45eda5b 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -535,6 +535,7 @@ repo_utils = struct( mkdir = _mkdir, norm_path = _norm_path, relative_to = _relative_to, + is_relative_to = _is_relative_to, repo_root_relative_path = _repo_root_relative_path, which_checked = _which_checked, which_unchecked = _which_unchecked, diff --git a/tests/repo_utils/repo_utils_test.bzl b/tests/repo_utils/repo_utils_test.bzl index 33ef298f0d..a72dba370f 100644 --- a/tests/repo_utils/repo_utils_test.bzl +++ b/tests/repo_utils/repo_utils_test.bzl @@ -36,10 +36,18 @@ def _test_relative_to(env): _tests.append(_test_relative_to) def _test_is_relative_to(env): - # Note: `_is_relative_to` isn't publicly exported in `repo_utils` struct natively. - # Actually wait, `is_relative_to` is not exported by repo_utils.bzl! - # Let's skip testing `_is_relative_to` directly in this suite unless we export it or we don't need to test it directly. - pass + mock_mrctx_linux = mocks.rctx(os_name = "linux") + mock_mrctx_win = mocks.rctx(os_name = "windows") + + # Case-sensitive matching (Linux) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar")).equals(True) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "qux")).equals(False) + + # Case-insensitive matching (Windows) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar")).equals(True) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "D:/Foo")).equals(False) + +_tests.append(_test_is_relative_to) def repo_utils_test_suite(name): """Create the test suite. From 5dc5668bb554edd8c0700880cc54638d2a55819f Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 02:23:52 -0700 Subject: [PATCH 06/10] format --- tests/repo_utils/repo_utils_test.bzl | 109 ++++++++++++++------------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/tests/repo_utils/repo_utils_test.bzl b/tests/repo_utils/repo_utils_test.bzl index a72dba370f..ce9e48b5a6 100644 --- a/tests/repo_utils/repo_utils_test.bzl +++ b/tests/repo_utils/repo_utils_test.bzl @@ -1,58 +1,59 @@ -"""Unit tests for repo_utils.bzl.""" - -load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility -load("//tests/support:mocks.bzl", "mocks") - -_tests = [] - -def _test_get_platforms_os_name(env): - mock_mrctx = mocks.rctx(os_name = "Mac OS X") - got = repo_utils.get_platforms_os_name(mock_mrctx) - env.expect.that_str(got).equals("osx") - -_tests.append(_test_get_platforms_os_name) - -def _test_relative_to(env): - mock_mrctx_linux = mocks.rctx(os_name = "linux") - mock_mrctx_win = mocks.rctx(os_name = "windows") - - # Case-sensitive matching (Linux) - got = repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar") - env.expect.that_str(got).equals("baz") - - # Case-insensitive matching (Windows) - got = repo_utils.relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar") - env.expect.that_str(got).equals("Baz") - - # Failure case - failures = [] - def _mock_fail(msg): - failures.append(msg) - - repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "qux", fail = _mock_fail) - env.expect.that_collection(failures).contains_exactly(["foo/bar/baz is not relative to qux"]) - -_tests.append(_test_relative_to) - -def _test_is_relative_to(env): - mock_mrctx_linux = mocks.rctx(os_name = "linux") - mock_mrctx_win = mocks.rctx(os_name = "windows") - - # Case-sensitive matching (Linux) - env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar")).equals(True) - env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "qux")).equals(False) - - # Case-insensitive matching (Windows) - env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar")).equals(True) - env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "D:/Foo")).equals(False) - -_tests.append(_test_is_relative_to) - -def repo_utils_test_suite(name): +"""Unit tests for repo_utils.bzl.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility +load("//tests/support:mocks.bzl", "mocks") + +_tests = [] + +def _test_get_platforms_os_name(env): + mock_mrctx = mocks.rctx(os_name = "Mac OS X") + got = repo_utils.get_platforms_os_name(mock_mrctx) + env.expect.that_str(got).equals("osx") + +_tests.append(_test_get_platforms_os_name) + +def _test_relative_to(env): + mock_mrctx_linux = mocks.rctx(os_name = "linux") + mock_mrctx_win = mocks.rctx(os_name = "windows") + + # Case-sensitive matching (Linux) + got = repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar") + env.expect.that_str(got).equals("baz") + + # Case-insensitive matching (Windows) + got = repo_utils.relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar") + env.expect.that_str(got).equals("Baz") + + # Failure case + failures = [] + + def _mock_fail(msg): + failures.append(msg) + + repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "qux", fail = _mock_fail) + env.expect.that_collection(failures).contains_exactly(["foo/bar/baz is not relative to qux"]) + +_tests.append(_test_relative_to) + +def _test_is_relative_to(env): + mock_mrctx_linux = mocks.rctx(os_name = "linux") + mock_mrctx_win = mocks.rctx(os_name = "windows") + + # Case-sensitive matching (Linux) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar")).equals(True) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "qux")).equals(False) + + # Case-insensitive matching (Windows) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar")).equals(True) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "D:/Foo")).equals(False) + +_tests.append(_test_is_relative_to) + +def repo_utils_test_suite(name): """Create the test suite. Args: name: the name of the test suite - """ - test_suite(name = name, basic_tests = _tests) + """ + test_suite(name = name, basic_tests = _tests) From 48fbf1dfb941b0611264b2b20f2f48fe35fb1f68 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 02:25:58 -0700 Subject: [PATCH 07/10] format --- tests/support/mocks.bzl | 42 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/support/mocks.bzl b/tests/support/mocks.bzl index d6f6a59250..fdcf55ec4b 100644 --- a/tests/support/mocks.bzl +++ b/tests/support/mocks.bzl @@ -1,6 +1,6 @@ -"""Mocks for testing.""" - -def _rctx(os_name = "linux", os_arch = "x86_64", environ = None, **kwargs): +"""Mocks for testing.""" + +def _rctx(os_name = "linux", os_arch = "x86_64", environ = None, **kwargs): """Creates a mock of repository_ctx or module_ctx. Args: @@ -11,21 +11,21 @@ def _rctx(os_name = "linux", os_arch = "x86_64", environ = None, **kwargs): Returns: A struct mocking repository_ctx. - """ - if environ == None: - environ = {} - - attrs = { - "os": struct( - name = os_name, - arch = os_arch, - ), - "getenv": environ.get, - } - attrs.update(kwargs) - - return struct(**attrs) - -mocks = struct( - rctx = _rctx, -) + """ + if environ == None: + environ = {} + + attrs = { + "os": struct( + name = os_name, + arch = os_arch, + ), + "getenv": environ.get, + } + attrs.update(kwargs) + + return struct(**attrs) + +mocks = struct( + rctx = _rctx, +) From 4dcbd6e95234947c263b48e033a40e44bce91729 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 02:27:11 -0700 Subject: [PATCH 08/10] format --- tests/support/mocks.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/support/mocks.bzl b/tests/support/mocks.bzl index fdcf55ec4b..44d3dc6faa 100644 --- a/tests/support/mocks.bzl +++ b/tests/support/mocks.bzl @@ -16,11 +16,11 @@ def _rctx(os_name = "linux", os_arch = "x86_64", environ = None, **kwargs): environ = {} attrs = { + "getenv": environ.get, "os": struct( name = os_name, arch = os_arch, ), - "getenv": environ.get, } attrs.update(kwargs) From 84d929b518e58063d9b94c583259fd5c98114207 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 02:32:22 -0700 Subject: [PATCH 09/10] format --- tests/repo_utils/BUILD.bazel | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/repo_utils/BUILD.bazel b/tests/repo_utils/BUILD.bazel index c1b1bfc304..74e8e37489 100644 --- a/tests/repo_utils/BUILD.bazel +++ b/tests/repo_utils/BUILD.bazel @@ -1,3 +1,3 @@ -load(":repo_utils_test.bzl", "repo_utils_test_suite") - -repo_utils_test_suite(name = "repo_utils_tests") +load(":repo_utils_test.bzl", "repo_utils_test_suite") + +repo_utils_test_suite(name = "repo_utils_tests") From bad3352e8b035e61cc7c24cc09a1ed89379117ce Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 11 Apr 2026 11:47:36 -0700 Subject: [PATCH 10/10] remove window CR --- tests/support/mocks.bzl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/support/mocks.bzl b/tests/support/mocks.bzl index 44d3dc6faa..2a4ccd0fc4 100644 --- a/tests/support/mocks.bzl +++ b/tests/support/mocks.bzl @@ -1,16 +1,16 @@ """Mocks for testing.""" def _rctx(os_name = "linux", os_arch = "x86_64", environ = None, **kwargs): - """Creates a mock of repository_ctx or module_ctx. - - Args: - os_name: The OS name to mock (e.g., "linux", "Mac OS X", "windows"). - os_arch: The OS architecture to mock (e.g., "x86_64", "aarch64"). - environ: A dictionary representing the environment variables. - **kwargs: Additional attributes to add to the mock struct. - - Returns: - A struct mocking repository_ctx. + """Creates a mock of repository_ctx or module_ctx. + + Args: + os_name: The OS name to mock (e.g., "linux", "Mac OS X", "windows"). + os_arch: The OS architecture to mock (e.g., "x86_64", "aarch64"). + environ: A dictionary representing the environment variables. + **kwargs: Additional attributes to add to the mock struct. + + Returns: + A struct mocking repository_ctx. """ if environ == None: environ = {}