aboutsummaryrefslogtreecommitdiff
path: root/python/pip_install/tools/wheel_installer/namespace_pkgs.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/pip_install/tools/wheel_installer/namespace_pkgs.py')
-rw-r--r--python/pip_install/tools/wheel_installer/namespace_pkgs.py121
1 files changed, 121 insertions, 0 deletions
diff --git a/python/pip_install/tools/wheel_installer/namespace_pkgs.py b/python/pip_install/tools/wheel_installer/namespace_pkgs.py
new file mode 100644
index 0000000..7d23c0e
--- /dev/null
+++ b/python/pip_install/tools/wheel_installer/namespace_pkgs.py
@@ -0,0 +1,121 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utility functions to discover python package types"""
+import os
+import textwrap
+from pathlib import Path # supported in >= 3.4
+from typing import List, Optional, Set
+
+
+def implicit_namespace_packages(
+ directory: str, ignored_dirnames: Optional[List[str]] = None
+) -> Set[Path]:
+ """Discovers namespace packages implemented using the 'native namespace packages' method.
+
+ AKA 'implicit namespace packages', which has been supported since Python 3.3.
+ See: https://packaging.python.org/guides/packaging-namespace-packages/#native-namespace-packages
+
+ Args:
+ directory: The root directory to recursively find packages in.
+ ignored_dirnames: A list of directories to exclude from the search
+
+ Returns:
+ The set of directories found under root to be packages using the native namespace method.
+ """
+ namespace_pkg_dirs: Set[Path] = set()
+ standard_pkg_dirs: Set[Path] = set()
+ directory_path = Path(directory)
+ ignored_dirname_paths: List[Path] = [Path(p) for p in ignored_dirnames or ()]
+ # Traverse bottom-up because a directory can be a namespace pkg because its child contains module files.
+ for dirpath, dirnames, filenames in map(
+ lambda t: (Path(t[0]), *t[1:]), os.walk(directory_path, topdown=False)
+ ):
+ if "__init__.py" in filenames:
+ standard_pkg_dirs.add(dirpath)
+ continue
+ elif ignored_dirname_paths:
+ is_ignored_dir = dirpath in ignored_dirname_paths
+ child_of_ignored_dir = any(
+ d in dirpath.parents for d in ignored_dirname_paths
+ )
+ if is_ignored_dir or child_of_ignored_dir:
+ continue
+
+ dir_includes_py_modules = _includes_python_modules(filenames)
+ parent_of_namespace_pkg = any(
+ Path(dirpath, d) in namespace_pkg_dirs for d in dirnames
+ )
+ parent_of_standard_pkg = any(
+ Path(dirpath, d) in standard_pkg_dirs for d in dirnames
+ )
+ parent_of_pkg = parent_of_namespace_pkg or parent_of_standard_pkg
+ if (
+ (dir_includes_py_modules or parent_of_pkg)
+ and
+ # The root of the directory should never be an implicit namespace
+ dirpath != directory_path
+ ):
+ namespace_pkg_dirs.add(dirpath)
+ return namespace_pkg_dirs
+
+
+def add_pkgutil_style_namespace_pkg_init(dir_path: Path) -> None:
+ """Adds 'pkgutil-style namespace packages' init file to the given directory
+
+ See: https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages
+
+ Args:
+ dir_path: The directory to create an __init__.py for.
+
+ Raises:
+ ValueError: If the directory already contains an __init__.py file
+ """
+ ns_pkg_init_filepath = os.path.join(dir_path, "__init__.py")
+
+ if os.path.isfile(ns_pkg_init_filepath):
+ raise ValueError("%s already contains an __init__.py file." % dir_path)
+
+ with open(ns_pkg_init_filepath, "w") as ns_pkg_init_f:
+ # See https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages
+ ns_pkg_init_f.write(
+ textwrap.dedent(
+ """\
+ # __path__ manipulation added by bazelbuild/rules_python to support namespace pkgs.
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
+ """
+ )
+ )
+
+
+def _includes_python_modules(files: List[str]) -> bool:
+ """
+ In order to only transform directories that Python actually considers namespace pkgs
+ we need to detect if a directory includes Python modules.
+
+ Which files are loadable as modules is extension based, and the particular set of extensions
+ varies by platform.
+
+ See:
+ 1. https://github.com/python/cpython/blob/7d9d25dbedfffce61fc76bc7ccbfa9ae901bf56f/Lib/importlib/machinery.py#L19
+ 2. PEP 420 -- Implicit Namespace Packages, Specification - https://www.python.org/dev/peps/pep-0420/#specification
+ 3. dynload_shlib.c and dynload_win.c in python/cpython.
+ """
+ module_suffixes = {
+ ".py", # Source modules
+ ".pyc", # Compiled bytecode modules
+ ".so", # Unix extension modules
+ ".pyd", # https://docs.python.org/3/faq/windows.html#is-a-pyd-file-the-same-as-a-dll
+ }
+ return any(Path(f).suffix in module_suffixes for f in files)