diff --git a/README.md b/README.md index 5cecc86..5f49933 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ poetry bundle venv .build/.venv --without dev package-python-function .build/.venv --output-dir .build/lambda ``` -The output will be a .zip file with the same name as your project from your pyproject.toml file. +The output will be a .zip file with the same name as your project from your pyproject.toml file (with dashes replaced +with underscores). ## Installation Use [pipx](https://github.com/pypa/pipx) to install: @@ -40,7 +41,7 @@ One of the following must be specified: - `--output`: The full output path of the final zip file. - `--output-dir`: The output directory for the final zip file. The name of the zip file will be based on the project's -name in the pyproject.toml file. +name in the pyproject.toml file (with dashes replaced with underscores). diff --git a/package_python_function/packager.py b/package_python_function/packager.py index a5b2feb..b3d1d7d 100644 --- a/package_python_function/packager.py +++ b/package_python_function/packager.py @@ -18,7 +18,7 @@ def __init__(self, venv_path: Path, project_path: Path, output_dir: Path, output self.venv_path = venv_path self.output_dir = output_file.parent if output_file else output_dir - self.output_file = output_file if output_file else output_dir / f'{self.project.name}.zip' + self.output_file = output_file if output_file else output_dir / f'{self.project.distribution_name}.zip' self._uncompressed_bytes = 0 diff --git a/package_python_function/python_project.py b/package_python_function/python_project.py index 60021a3..f12b563 100644 --- a/package_python_function/python_project.py +++ b/package_python_function/python_project.py @@ -2,6 +2,7 @@ from pathlib import Path from typing import Optional import tomllib +import re class PythonProject: @@ -16,6 +17,16 @@ def name(self) -> str: ('tool', 'poetry', 'name'), )) + """ + Get the normalized name of the distribution, according to the Python Packaging Authority (PyPa) guidelines. + This is used to create the name of the zip file. + The name is normalized by replacing any non-alphanumeric characters with underscores. + https://peps.python.org/pep-0427/#escaping-and-unicode + """ + @cached_property + def distribution_name(self) -> str: + return re.sub("[^\w\d.]+", "_", self.name, re.UNICODE) + @cached_property def entrypoint_package_name(self) -> str: """ @@ -23,7 +34,7 @@ def entrypoint_package_name(self) -> str: code. """ # TODO : Parse out the project's package dir(s) if defined. Use the first one if there are multiple. - return self.name.replace('-', '_') + return self.distribution_name def find_value(self, paths: tuple[tuple[str]]) -> str: for path in paths: diff --git a/scripts/poc/inner_package/other_package/other_package_module.py b/scripts/poc/inner_package/other_package/other_package_module.py index 76ca296..0d7dbb7 100644 --- a/scripts/poc/inner_package/other_package/other_package_module.py +++ b/scripts/poc/inner_package/other_package/other_package_module.py @@ -1,2 +1,8 @@ +import logging + +logger = logging.getLogger(__name__) + +logger.info("Load") + def other_package_module(): - print("other_package_module") \ No newline at end of file + logger.info("Hello from other_package_module") \ No newline at end of file diff --git a/scripts/poc/inner_package/zip_in_zip_test/__init__.py b/scripts/poc/inner_package/zip_in_zip_test/__init__.py index 03aa4d9..1dfb4b2 100644 --- a/scripts/poc/inner_package/zip_in_zip_test/__init__.py +++ b/scripts/poc/inner_package/zip_in_zip_test/__init__.py @@ -1,6 +1,10 @@ # This file represents the original module's __init__.py file that gets renamed when creating the innner ZIP. -print("__init__ original") +import logging + +logger = logging.getLogger(__name__ + "(__init__.py ORIGINAL)") + +logger.info("Hello, I am the original pacakge __init__.py") GLOBAL_VALUE_IN_INIT_ORIGINAL = "This global is defined in the original __init__.py" diff --git a/scripts/poc/inner_package/zip_in_zip_test/main.py b/scripts/poc/inner_package/zip_in_zip_test/main.py index 5192f03..3e6147f 100644 --- a/scripts/poc/inner_package/zip_in_zip_test/main.py +++ b/scripts/poc/inner_package/zip_in_zip_test/main.py @@ -1,10 +1,14 @@ -print("main.py: Load") +import logging + +logger = logging.getLogger(__name__) + +logger.info("main.py: Load") from zip_in_zip_test import GLOBAL_VALUE_IN_INIT_ORIGINAL, other_module_function from other_package.other_package_module import other_package_module def main(): - print("Hello from main!") - print(GLOBAL_VALUE_IN_INIT_ORIGINAL) + logger.info("Hello from main!") + logger.info(GLOBAL_VALUE_IN_INIT_ORIGINAL) other_module_function() other_package_module() \ No newline at end of file diff --git a/scripts/poc/inner_package/zip_in_zip_test/other_module.py b/scripts/poc/inner_package/zip_in_zip_test/other_module.py index 51e1982..0244bee 100644 --- a/scripts/poc/inner_package/zip_in_zip_test/other_module.py +++ b/scripts/poc/inner_package/zip_in_zip_test/other_module.py @@ -1,2 +1,8 @@ +import logging + +logger = logging.getLogger(__name__) + +logger.info("Load") + def other_module_function(): - print("I'm in other_module_function") \ No newline at end of file + logger.info("I'm in other_module_function") \ No newline at end of file diff --git a/scripts/poc/lambda-runner.py b/scripts/poc/lambda-runner.py index 836991d..4c0dc8d 100644 --- a/scripts/poc/lambda-runner.py +++ b/scripts/poc/lambda-runner.py @@ -1,12 +1,15 @@ # This is my best attempt at simulating what AWS Lambda does # Instead of messing with zipping and unzipping in this experiment, I just copy the files to the .test directory. +import logging from pathlib import Path import shutil import sys -print('[lambda-runner]') -print('sys.path:', sys.path) +logging.basicConfig(level=logging.INFO, stream=sys.stdout) +logger = logging.getLogger("lambda-runner.py") + +logger.info('BEGIN') module_path = Path(__file__).parent TEST_DIR = module_path / ".test" @@ -14,11 +17,19 @@ TEST_PACKAGE_DIR = TEST_DIR / PACKAGE_NAME shutil.rmtree(TEST_DIR, ignore_errors=True) + +# Copy the stub `zip_in_zip_test` package to the .test directory. This simulates the outer ZIP extraction +# that lambda will do. This is the module that Lambda will import, and where we will do the inner ZIP extraction. shutil.copytree(str(module_path / PACKAGE_NAME), str(TEST_PACKAGE_DIR)) + +# Copy the inner package to the .test directory. This simulates the inner ZIP file, but without actually dealing +# with zip/unzip in this experiemen. shutil.copytree(str(module_path / "inner_package"), str(TEST_PACKAGE_DIR / ".inner_package")) sys.path.insert(0, str(TEST_DIR)) +logger.info('--- Importing entrypoint module ---') import importlib module = importlib.import_module('zip_in_zip_test.main') +logger.info('--- Calling entryoint function ---') module.__dict__['main']() diff --git a/scripts/poc/zip_in_zip_test/__init__.py b/scripts/poc/zip_in_zip_test/__init__.py index 7f0186d..a326477 100644 --- a/scripts/poc/zip_in_zip_test/__init__.py +++ b/scripts/poc/zip_in_zip_test/__init__.py @@ -1,14 +1,20 @@ -# This works perfectly! - -print('zip_in_zip_test.__init__: BEGIN. This is the loader.') -print("module_path:", __file__) - +""" +Demonstrate how we can swap out the content an entire package during the __init__.py load process of that package. +""" from pathlib import Path import importlib import sys +import logging + +logger = logging.getLogger(__name__ + "(__init__.py loader)") + +logger.info(f'BEGIN. This is the loader. {__file__}') module_path = Path(__file__).parent +# This is where we would unzip the inner ZIP file. For this experiment, we can skip actually doing that and pretend +# that it was extracted to .inner_package/ + # This works if I insert at zero. # Why does the serverless-python-requirements insist on inserting at 1? # From https://docs.aws.amazon.com/lambda/latest/dg/python-package.html#python-package-searchpath: @@ -17,6 +23,7 @@ # This also works. I am thinking this is the best way, because we need to unmount the original decompressed directory # since it contains the load __init__.py. +previous_sys_path_root = sys.path[0] sys.path[0] = str(module_path / ".inner_package") @@ -30,6 +37,7 @@ # importlib.import_module(__name__) # This also works. I think this is the best way. +logger.info(f'Reloading {__name__} after switching {previous_sys_path_root} to {sys.path[0]}.') importlib.reload(sys.modules[__name__]) -print('zip_in_zip_test.__init__: END') \ No newline at end of file +logger.info('END') \ No newline at end of file diff --git a/tests/test_package_python_function.py b/tests/test_package_python_function.py index 88bdc43..5d0ba3a 100644 --- a/tests/test_package_python_function.py +++ b/tests/test_package_python_function.py @@ -34,4 +34,4 @@ def test_package_python_function(tmp_path: Path) -> None: ] main() - assert (output_dir_path / 'project-1.zip').exists() \ No newline at end of file + assert (output_dir_path / 'project_1.zip').exists() \ No newline at end of file