Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add checks to determine whether "create" and "modify" can safely run #11

Merged
merged 3 commits into from
Sep 29, 2023
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
79 changes: 79 additions & 0 deletions src/web/BL_Python/web/scaffolding/scaffolder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
import sys
import types
from dataclasses import asdict, dataclass, field
from importlib.machinery import SourceFileLoader
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from typing import Any, cast
Expand Down Expand Up @@ -313,7 +316,83 @@ def _scaffold_endpoints(
overwrite_existing_files=overwrite_existing_files,
)

# it is safe for create if:
# - the current directory is not a scaffolded application
# it is safe for modify if:
# - the <application name> directory exists
# - the <application name> directory is a scaffolded application
# - the scaffolder is run from the scaffolded application's parent directory
def _check_scaffolded_application_exists(self, test_path: Path):
"""
Determine whether the application <config.application_name> is an importable
BL_Python.web application that has been previously scaffolded with
this tool.

:param test_path: The directory to test for the existence of a scaffolded application.
"""
application_import_path = Path(".", test_path)
self._log.debug(
f"Adding `{application_import_path}` to process module import path."
)
# modifying the import path to include a relative directory
# is less than ideal and potentionally dangerous. we should
# consider forking here or doing something else to prevent
# the global modification.
sys.path.insert(0, str(application_import_path))
Copy link
Member

Choose a reason for hiding this comment

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

I know of the existence of importlib and a quick Googlin' turned this up - I make no promises that this is actually addressing this problem: https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately it's not the source file itself I need here - it's the package/module. Otherwise, SourceFileLoader.load_module would work and reduces the complexity of using spec_from_file_location directly (and which I'm using in the module hook lookup, which I should replace).

The package/module is needed because _version.py references the module's metadata with importlib.metadata to get the software version from pyproject.toml. Just loading the source file fails because a source file does not contain package metadata regardless of its location. So we have to fake that the package is installed in order for Python to treat the source file as if it's loaded from a package.

Copy link
Member

Choose a reason for hiding this comment

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

Alas! It seems my dashed-off suggestion is not the solution to all of your problems.


try:
# if any of these steps fail, then the `test_path`
# does not already contain a scaffolded application
loader = SourceFileLoader(
self._config.application_name,
str(
Path(
test_path,
self._config.application_name,
"__init__.py",
)
),
)
mod = types.ModuleType(loader.name)
sys.modules[self._config.application_name] = mod
module = loader.load_module()
_ = module.__version__
_ = module._version.__bl_python_scaffold__
# if no errors occur, then the `test_path`
# contains a scaffolded application
return True
except Exception as e:
self._log.debug(str(e), exc_info=True)
return False
finally:
if self._config.application_name in sys.modules:
del sys.modules[self._config.application_name]
popped_import_path = sys.path.pop(0)
self._log.debug(
f"Popped `{popped_import_path}` from process module import path. Expected to pop `{application_import_path}`."
)

def scaffold(self):
in_parent_directory = self._check_scaffolded_application_exists(
Path(self._config.output_directory)
)
in_application_directory = self._check_scaffolded_application_exists(Path("."))

# "create" can run from any directory that is not an existing
# application's root directory.
if self._config.mode == "create" and in_application_directory:
self._log.critical(
"Attmpted to scaffold a new application in the same directory as an existing application. This is not supported. Change your working directory to the application's parent directory, or run this from a directory that does not contain an existing application."
)
return
# modify can only run from an existing application's
# parent directory.
elif self._config.mode == "modify" and not in_parent_directory:
self._log.critical(
f"Attempted to modify an existing application from a directory that is not the existing application's parent directory. This is not supported. Change your working directory to the application's parent directory."
)
return

# used for the primary set of templates that a
# scaffolded application is made up of.
base_env = Environment(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
application_configs = []
application_modules = []
from {{application_name}}._version import __version__

{% if 'database' in module %}
from BL_Python.database.config import DatabaseConfig
from BL_Python.database.dependency_injection import ScopedSessionModule
application_configs.append(DatabaseConfig)
application_modules.append(ScopedSessionModule())
{% endif %}
def create_app():
application_configs = []
application_modules = []

from BL_Python.web.application import create_app
# fmt: off
app = create_app(
application_configs=application_configs,
application_modules=application_modules
)
# fmt: on
{% if 'database' in module %}
from BL_Python.database.config import DatabaseConfig
from BL_Python.database.dependency_injection import ScopedSessionModule
application_configs.append(DatabaseConfig)
application_modules.append(ScopedSessionModule())
{% endif %}

from BL_Python.web.application import create_app as _create_app
# fmt: off
return _create_app(
application_configs=application_configs,
application_modules=application_modules
)
# fmt: on
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __init__ import app
from __init__ import create_app

app.run()
if __name__ == '__main__':
create_app().run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import importlib.metadata

__version__ = importlib.metadata.version(__package__ or __name__)
__bl_python_scaffold__ = True
Loading