Skip to content

Commit

Permalink
fix: a more consise fix for namespace packages (#2665)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelotduarte committed Nov 5, 2024
1 parent 5dda41b commit 0e45caf
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 26 deletions.
31 changes: 15 additions & 16 deletions cx_Freeze/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def __init__(
self.zip_includes: InternalIncludesList = process_path_specs(
zip_includes
)
self.namespaces = []
self.modules = []
self.aliases = {}
self.excluded_dependent_files: set[Path] = set()
Expand Down Expand Up @@ -380,17 +381,18 @@ def _load_module(
spec = importlib.machinery.PathFinder.find_spec(name, path)
except KeyError:
if parent:
# some packages use a directory with vendored modules
# without an __init__.py and are not considered namespace
# packages, then simulate a subpackage
# some packages use a directory with vendor modules without
# an __init__.py, thus, are called nested namespace packages
module = self._add_module(
name,
path=[Path(path[0], name.rpartition(".")[-1])],
parent=parent,
)
logging.debug("Adding module [%s] [PACKAGE]", name)
module.file = Path(path[0]) / "__init__.py"
module.source_is_string = True
logging.debug("Adding module [%s] [NESTED NAMESPACE]", name)
self.namespaces.append(module)
self.modules.remove(module)
module.in_import = False
return module

if spec:
loader = spec.loader
Expand All @@ -408,11 +410,12 @@ def _load_module(
)
if spec.origin in (None, "namespace"):
logging.debug("Adding module [%s] [NAMESPACE]", name)
module.file = module.path[0] / "__init__.py"
module.source_is_string = True
else:
logging.debug("Adding module [%s] [PACKAGE]", name)
module.file = Path(spec.origin) # path of __init__.py
self.namespaces.append(module)
self.modules.remove(module)
module.in_import = False
return module
logging.debug("Adding module [%s] [PACKAGE]", name)
module.file = Path(spec.origin) # path of __init__.py
else:
module = self._add_module(
name, filename=Path(spec.origin), parent=parent
Expand Down Expand Up @@ -452,13 +455,9 @@ def _load_module_code(
raise ImportError(msg, name=name)
elif isinstance(loader, importlib.machinery.ExtensionFileLoader):
logging.debug("Adding module [%s] [EXTENSION]", name)
elif module.source_is_string:
module.code = compile(
"", path, "exec", dont_inherit=True, optimize=self.optimize
)
else:
msg = f"Unknown module loader in {path}"
raise ImportError(msg, name=name)
raise ImportError(msg, name=name) # noqa: TRY004

# Run custom hook for the module
if module.hook:
Expand Down
8 changes: 8 additions & 0 deletions cx_Freeze/freezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,14 @@ def _write_modules(self) -> None:
cache_path = finder.cache_path

modules = [m for m in finder.modules if m.name not in finder.excludes]
for module in finder.namespaces:
# if namespace package should be written to zip file, convert it
# to regular package, since then zipimport doesn't support PEP420
if module.in_file_system == 0:
module.code = compile(
"", "__init__.py", "exec", dont_inherit=True
)
modules.append(module)
modules.append(
finder.include_file_as_module(
self.constants_module.create(cache_path, modules)
Expand Down
13 changes: 8 additions & 5 deletions cx_Freeze/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,18 +196,21 @@ def __init__(
self.global_names: set[str] = set()
self.ignore_names: set[str] = set()
self.in_import: bool = True
self.source_is_string: bool = False
self.source_is_zip_file: bool = False
self._in_file_system: int = 1
# add the load hook
self.load_hook()

def __repr__(self) -> str:
parts = [f"name={self.name!r}"]
if self.distribution is not None:
parts.append(f"distribution={self.distribution.name!r}")
if self.file is not None:
parts.append(f"file={self.file!r}")
parts.append(f"file={self.file.as_posix()!r}")
if self.path is not None:
parts.append(f"path={self.path!r}")
parts.append(f"path={self.path}")
if self.parent is not None:
parts.append(f"parent.name={self.parent.name!r}")
join_parts = ", ".join(parts)
return f"<Module {join_parts}>"

Expand Down Expand Up @@ -314,7 +317,7 @@ def in_file_system(self) -> int:
"""
if self.parent is not None:
return self.parent.in_file_system
if self.path is None or self.file is None:
if self.path is None:
return 0
return self._in_file_system

Expand Down Expand Up @@ -440,7 +443,7 @@ def create(self, tmp_path: Path, modules: list[Module]) -> Path:
today = datetime.now(tz=timezone.utc)
source_timestamp = 0
for module in modules:
if module.file is None or module.source_is_string:
if module.file is None:
continue
if module.source_is_zip_file:
continue
Expand Down
29 changes: 29 additions & 0 deletions tests/generate_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,35 @@
""",
]

NAMESPACE_TEST = [
"main",
["main", "namespace.package"],
[],
[],
"""\
main.py
import namespace.package
namespace/package/__init__.py
print('This is namespace.package')
""",
]

NESTED_NAMESPACE_TEST = [
"main",
["main", "namespace.package.one", "namespace.package.two"],
[],
[],
"""\
main.py
import namespace.package.one
import namespace.package.two
namespace/package/one.py
print('This is namespace.package module one')
namespace/package/two.py
print('This is namespace.package module two')
""",
]

PACKAGE_TEST = [
"a.module",
["a", "a.b", "a.c", "a.module", "mymodule", "sys"],
Expand Down
113 changes: 108 additions & 5 deletions tests/test_executables.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,18 +164,25 @@ def func1():
def func2():
print("Test freeze module #2")
setup.py
from cx_Freeze import Executable, setup
from cx_Freeze import setup
executables = ["test_1.py", "test_2.py", "test_3.py"]
options = {
"build_exe": {
"excludes": ["tkinter", "unittest"],
"include_path": ["."],
"silent": True
}
}
setup(
name="advanced",
version="0.1.2.3",
description="Sample cx_Freeze script",
executables=executables,
options=options,
executables=["test_1.py", "test_2.py", "test_3.py"],
)
command
python setup.py build_exe --excludes=tkinter,unittest --silent
python setup.py build_exe
"""


Expand Down Expand Up @@ -338,7 +345,6 @@ def test_not_found_icon(tmp_path: Path) -> None:
# same test as before, without icons
create_package(tmp_path, SOURCE_VALID_ICON)
output = run_command(tmp_path)
print(output)
assert "WARNING: Icon file not found" in output, "icon file not found"


Expand Down Expand Up @@ -407,3 +413,100 @@ def test_executable_rename(tmp_path: Path) -> None:
)
output = run_command(tmp_path, file_renamed, timeout=10)
assert output.startswith("Hello from cx_Freeze")


SOURCE_NAMESPACE = """\
main.py
import importlib.util
import namespace.package
def is_namespace_package(package_name: str) -> bool:
spec = importlib.util.find_spec(package_name)
return spec.origin is None
if __name__ == "__main__":
print("'namespace' is namespace package: ",
is_namespace_package('namespace'))
print("'namespace.package' is namespace package: ",
is_namespace_package('namespace.package'))
namespace/package/__init__.py
print("Hello from cx_Freeze")
command
cxfreeze --script main.py --target-name test --silent
"""

SOURCE_NESTED_NAMESPACE = """\
main.py
import importlib.util
import namespace.package.one
import namespace.package.two
def is_namespace_package(package_name: str) -> bool:
spec = importlib.util.find_spec(package_name)
return spec.origin is None
if __name__ == "__main__":
print("'namespace' is namespace package: ",
is_namespace_package('namespace'))
print("'namespace.package' is namespace package: ",
is_namespace_package('namespace.package'))
print("'namespace.package.one' is namespace package: ",
is_namespace_package('namespace.package.one'))
print("'namespace.package.two' is namespace package: ",
is_namespace_package('namespace.package.two'))
namespace/package/one.py
print("Hello from cx_Freeze - module one")
namespace/package/two.py
print("Hello from cx_Freeze - module two")
command
cxfreeze --script main.py --target-name test --silent
"""


@pytest.mark.parametrize(
("source", "hello", "namespace", "package_or_module", "zip_packages"),
[
(SOURCE_NAMESPACE, 1, 1, 1, False),
(SOURCE_NAMESPACE, 1, 0, 2, True),
(SOURCE_NESTED_NAMESPACE, 2, 2, 2, False),
(SOURCE_NESTED_NAMESPACE, 2, 0, 4, True),
],
ids=[
"namespace_package",
"namespace_package_zip_packages",
"nested_namespace_package",
"nested_namespace_package_zip_packages",
],
)
def test_executable_namespace(
tmp_path: Path,
source: str,
hello: int,
namespace: int,
package_or_module: int,
zip_packages: bool,
) -> None:
"""Test executable with namespace package."""
create_package(tmp_path, source)
if zip_packages:
with tmp_path.joinpath("command").open("a") as f:
f.write(" --zip-include-packages=* --zip-exclude-packages=")
output = run_command(tmp_path)

file_created = tmp_path / BUILD_EXE_DIR / f"test{EXE_SUFFIX}"
assert file_created.is_file(), f"file not found: {file_created}"

output = run_command(tmp_path, file_created, timeout=10)
lines = output.splitlines()
start = 0
stop = hello
for i in range(start, stop):
assert lines[i].startswith("Hello from cx_Freeze")
start += hello
stop += namespace
for i in range(start, stop):
assert lines[i].endswith("True")
start += namespace
stop += package_or_module
for i in range(start, stop):
assert lines[i].endswith("False")
8 changes: 8 additions & 0 deletions tests/test_modulefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
IMPORT_CALL_TEST,
MAYBE_TEST,
MAYBE_TEST_NEW,
NAMESPACE_TEST,
NESTED_NAMESPACE_TEST,
PACKAGE_TEST,
RELATIVE_IMPORT_TEST,
RELATIVE_IMPORT_TEST_2,
Expand Down Expand Up @@ -82,6 +84,8 @@ def _do_test(
IMPORT_CALL_TEST,
MAYBE_TEST,
MAYBE_TEST_NEW,
NAMESPACE_TEST,
NESTED_NAMESPACE_TEST,
PACKAGE_TEST,
RELATIVE_IMPORT_TEST,
RELATIVE_IMPORT_TEST_2,
Expand All @@ -99,6 +103,8 @@ def _do_test(
"import_call_test",
"maybe_test",
"maybe_test_new",
"namespace_test",
"nested_namespace_test",
"package_test",
"relative_import_test",
"relative_import_test_2",
Expand Down Expand Up @@ -139,6 +145,7 @@ def test_zip_include_packages(tmp_path) -> None:
*SUB_PACKAGE_TEST,
zip_exclude_packages=["*"],
zip_include_packages=["p"],
zip_include_all_packages=False,
)


Expand All @@ -149,4 +156,5 @@ def test_zip_exclude_packages(tmp_path) -> None:
*SUB_PACKAGE_TEST,
zip_exclude_packages=["p"],
zip_include_packages=["*"],
zip_include_all_packages=True,
)

0 comments on commit 0e45caf

Please sign in to comment.