From 3d511ff727bf4feaf8141a5dae69e2fc2ac810fe Mon Sep 17 00:00:00 2001 From: Sven Heidemann Date: Tue, 30 Mar 2021 11:53:07 +0200 Subject: [PATCH] Bugfixes --- cpl.json | 1 + src/cpl_cli/command/help_service.py | 2 +- src/cpl_cli/command/new_service.py | 5 +- src/cpl_cli/configuration/build_settings.py | 7 + .../configuration/build_settings_name_enum.py | 1 + .../configuration/project_type_enum.py | 7 + src/cpl_cli/configuration/settings_helper.py | 1 + src/cpl_cli/publish/publisher_service.py | 10 +- test_2021_4/test1/old_ps.py | 419 ++++++++++++++++++ 9 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 src/cpl_cli/configuration/project_type_enum.py create mode 100644 test_2021_4/test1/old_ps.py diff --git a/cpl.json b/cpl.json index f479eab1..007b57db 100644 --- a/cpl.json +++ b/cpl.json @@ -34,6 +34,7 @@ "Classifiers": [] }, "BuildSettings": { + "ProjectType": "library", "SourcePath": "src", "OutputPath": "dist", "Main": "cpl_cli.main", diff --git a/src/cpl_cli/command/help_service.py b/src/cpl_cli/command/help_service.py index 647640f5..5088b379 100644 --- a/src/cpl_cli/command/help_service.py +++ b/src/cpl_cli/command/help_service.py @@ -24,7 +24,7 @@ class HelpService(CommandABC): ['help (h|H)', 'Lists available command and their short descriptions.'], ['install (i|I)', 'With argument installs packages to project, without argument installs project dependencies.'], ['new (n|N)', 'Creates new CPL project.'], - ['publish (p|P)', 'Prepares files for publish into an output directory named dist/ at the given output path and executes setup_template.py. Must be executed from within a workspace directory.'], + ['publish (p|P)', 'Prepares files for publish into an output directory named dist/ at the given output path and executes setup.py. Must be executed from within a library workspace directory.'], ['start (s|S)', 'Starts CPL project, restarting on file changes'], ['uninstall (ui|UI)', 'Uninstalls packages from project.'], ['update (u|u)', 'Update CPL and project dependencies.'], diff --git a/src/cpl_cli/command/new_service.py b/src/cpl_cli/command/new_service.py index 0926b4c9..8b3c0a65 100644 --- a/src/cpl_cli/command/new_service.py +++ b/src/cpl_cli/command/new_service.py @@ -97,6 +97,7 @@ class NewService(CommandABC): def _create_build_settings(self): self._build_dict = { + BuildSettingsNameEnum.project_type.value: self._command, BuildSettingsNameEnum.source_path.value: 'src', BuildSettingsNameEnum.output_path.value: 'dist', BuildSettingsNameEnum.main.value: 'main', @@ -244,7 +245,7 @@ class NewService(CommandABC): """ if len(args) == 0: self._help('Usage: cpl new [options]') - exit() + return self._command = args[0] if self._command == 'console': @@ -252,4 +253,4 @@ class NewService(CommandABC): else: self._help('Usage: cpl new [options]') - exit() + return diff --git a/src/cpl_cli/configuration/build_settings.py b/src/cpl_cli/configuration/build_settings.py index 641a4a15..8c9e34c0 100644 --- a/src/cpl_cli/configuration/build_settings.py +++ b/src/cpl_cli/configuration/build_settings.py @@ -5,6 +5,7 @@ from cpl.configuration.configuration_model_abc import ConfigurationModelABC from cpl.console.console import Console from cpl.console.foreground_color_enum import ForegroundColorEnum from cpl_cli.configuration.build_settings_name_enum import BuildSettingsNameEnum +from cpl_cli.configuration.project_type_enum import ProjectTypeEnum class BuildSettings(ConfigurationModelABC): @@ -12,6 +13,7 @@ class BuildSettings(ConfigurationModelABC): def __init__(self): ConfigurationModelABC.__init__(self) + self._project_type: Optional[ProjectTypeEnum] = None self._source_path: Optional[str] = None self._output_path: Optional[str] = None self._main: Optional[str] = None @@ -21,6 +23,10 @@ class BuildSettings(ConfigurationModelABC): self._excluded: Optional[list[str]] = None self._package_data: Optional[dict[str, list[str]]] = None + @property + def project_type(self): + return self._project_type + @property def source_path(self) -> str: return self._source_path @@ -55,6 +61,7 @@ class BuildSettings(ConfigurationModelABC): def from_dict(self, settings: dict): try: + self._project_type = settings[BuildSettingsNameEnum.project_type.value] self._source_path = settings[BuildSettingsNameEnum.source_path.value] self._output_path = settings[BuildSettingsNameEnum.output_path.value] self._include_package_data = bool(settings[BuildSettingsNameEnum.include_package_data.value]) diff --git a/src/cpl_cli/configuration/build_settings_name_enum.py b/src/cpl_cli/configuration/build_settings_name_enum.py index ae5719b5..18d78749 100644 --- a/src/cpl_cli/configuration/build_settings_name_enum.py +++ b/src/cpl_cli/configuration/build_settings_name_enum.py @@ -3,6 +3,7 @@ from enum import Enum class BuildSettingsNameEnum(Enum): + project_type = 'ProjectType' source_path = 'SourcePath' output_path = 'OutputPath' main = 'Main' diff --git a/src/cpl_cli/configuration/project_type_enum.py b/src/cpl_cli/configuration/project_type_enum.py new file mode 100644 index 00000000..9471c59c --- /dev/null +++ b/src/cpl_cli/configuration/project_type_enum.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class ProjectTypeEnum(Enum): + + console = 'console' + library = 'library' diff --git a/src/cpl_cli/configuration/settings_helper.py b/src/cpl_cli/configuration/settings_helper.py index deb75a10..e4229445 100644 --- a/src/cpl_cli/configuration/settings_helper.py +++ b/src/cpl_cli/configuration/settings_helper.py @@ -34,6 +34,7 @@ class SettingsHelper: @staticmethod def get_build_settings_dict(build: BuildSettings) -> dict: return { + BuildSettingsNameEnum.project_type.value: build.project_type, BuildSettingsNameEnum.source_path.value: build.source_path, BuildSettingsNameEnum.output_path.value: build.output_path, BuildSettingsNameEnum.main.value: build.main, diff --git a/src/cpl_cli/publish/publisher_service.py b/src/cpl_cli/publish/publisher_service.py index a2905d25..f255411f 100644 --- a/src/cpl_cli/publish/publisher_service.py +++ b/src/cpl_cli/publish/publisher_service.py @@ -12,6 +12,7 @@ from cpl.console.console import Console from cpl.environment.application_environment_abc import ApplicationEnvironmentABC from cpl_cli.configuration.build_settings import BuildSettings from cpl_cli.configuration.project_settings import ProjectSettings +from cpl_cli.configuration.project_type_enum import ProjectTypeEnum from cpl_cli.publish.publisher_abc import PublisherABC from cpl_cli.templates.build.init_template import InitTemplate from cpl_cli.templates.publish.setup_template import SetupTemplate @@ -287,14 +288,13 @@ class PublisherService(PublisherABC): os.remove(setup_file) main = None - main_not_found = False try: main = importlib.import_module(self._build_settings.main) except Exception as e: Console.error('Could not find entry point', str(e)) - main_not_found = True + return - if (main is None or not hasattr(main, 'main')) and not main_not_found: + if main is None or not hasattr(main, 'main'): Console.error('Could not find entry point') return @@ -388,6 +388,10 @@ class PublisherService(PublisherABC): 4. Remove all included source from dist_path/publish :return: """ + if self._build_settings.project_type != ProjectTypeEnum.library.value: + Console.error(f'Project must be a {ProjectTypeEnum.library.value} for publishing.') + return + self._output_path = os.path.join(self._output_path, 'publish') Console.write_line('Build:') diff --git a/test_2021_4/test1/old_ps.py b/test_2021_4/test1/old_ps.py new file mode 100644 index 00000000..482a716d --- /dev/null +++ b/test_2021_4/test1/old_ps.py @@ -0,0 +1,419 @@ +import importlib +import os +import shutil +import sys +from string import Template as stringTemplate + +import setuptools +from packaging import version +from setuptools import sandbox + +from cpl.console.foreground_color_enum import ForegroundColorEnum +from cpl.console.console import Console +from cpl.environment.application_environment_abc import ApplicationEnvironmentABC +from cpl_cli.configuration.build_settings import BuildSettings +from cpl_cli.configuration.project_settings import ProjectSettings +from cpl_cli.publish.publisher_abc import PublisherABC +from cpl_cli.templates.build.init_template import InitTemplate +from cpl_cli.templates.publish.setup_template import SetupTemplate + + +class PublisherService(PublisherABC): + + def __init__(self, env: ApplicationEnvironmentABC, project: ProjectSettings, build: BuildSettings): + """ + Service to build or publish files for distribution + :param env: + :param project: + :param build: + """ + PublisherABC.__init__(self) + + self._env = env + self._project_settings = project + self._build_settings = build + + self._source_path = os.path.join(self._env.working_directory, self._build_settings.source_path) + self._output_path = os.path.join(self._env.working_directory, self._build_settings.output_path) + + self._included_files: list[str] = [] + self._included_dirs: list[str] = [] + self._distributed_files: list[str] = [] + + @property + def source_path(self) -> str: + return self._source_path + + @property + def dist_path(self) -> str: + return self._output_path + + @staticmethod + def _get_module_name_from_dirs(file: str) -> str: + """ + Extracts module name from directories + :param file: + :return: + """ + if 'src/' in file: + file = file.replace('src/', '', 1) + + dirs = os.path.dirname(file).split('/') + for d in dirs: + if d.__contains__('.'): + dirs.remove(d) + + if len(dirs) == 0: + return os.path.basename(file) + else: + return '.'.join(dirs) + + @staticmethod + def _delete_path(path: str): + """ + Deletes full path tree + :param path: + :return: + """ + if os.path.isdir(path): + try: + shutil.rmtree(path) + except Exception as e: + Console.error(f'{e}') + exit() + + @staticmethod + def _create_path(path: str): + """ + Creates full path tree + :param path: + :return: + """ + if not os.path.isdir(path): + try: + os.makedirs(path) + except Exception as e: + Console.error(f'{e}') + exit() + + def _is_path_included(self, path: str) -> bool: + """ + Checks if the path is included + :param path: + :return: + """ + for included in self._build_settings.included: + if included.startswith('*'): + included = included.replace('*', '') + + if included in path and path not in self._build_settings.excluded: + return True + + return False + + def _is_path_excluded(self, path: str) -> bool: + """ + Checks if the path is excluded + :param path: + :return: + """ + for excluded in self._build_settings.excluded: + if excluded.startswith('*'): + excluded = excluded.replace('*', '') + + if excluded in path and not self._is_path_included(path): + return True + + return False + + def _read_sources(self): + """ + Reads all source files and save included files + :return: + """ + for file in self._build_settings.included: + rel_path = os.path.relpath(file) + if os.path.isdir(rel_path): + for r, d, f in os.walk(rel_path): + for sub_file in f: + relative_path = os.path.relpath(r) + file_path = os.path.join(relative_path, os.path.relpath(sub_file)) + + print(file_path) + self._included_files.append(os.path.relpath(file_path)) + + elif os.path.isfile(rel_path): + print(rel_path) + self._included_files.append(rel_path) + + for r, d, f in os.walk(self._build_settings.source_path): + for file in f: + relative_path = os.path.relpath(r) + file_path = os.path.join(relative_path, os.path.relpath(file)) + if self._is_path_excluded(relative_path): + break + + if len(d) > 0: + for directory in d: + empty_dir = os.path.join(os.path.dirname(file_path), directory) + if len(os.listdir(empty_dir)) == 0: + self._included_dirs.append(empty_dir) + + if not self._is_path_excluded(relative_path): + self._included_files.append(os.path.relpath(file_path)) + + def _create_packages(self): + """ + Writes information from template to all included __init__.py + :return: + """ + for file in self._included_files: + if file.endswith('__init__.py'): + template_content = '' + module_file_lines: list[str] = [] + + title = self._get_module_name_from_dirs(file) + if title == '': + title = self._project_settings.name + elif not title.__contains__('.'): + title = f'{self._project_settings.name}.{title}' + + module_py_lines: list[str] = [] + imports = '' + + with open(file, 'r') as py_file: + module_file_lines = py_file.readlines() + py_file.close() + + if len(module_file_lines) == 0: + imports = '# imports:' + else: + is_started = False + for line in module_file_lines: + if line.__contains__('# imports'): + is_started = True + + if (line.__contains__('from') or line.__contains__('import')) and is_started: + module_py_lines.append(line.replace('\n', '')) + + if len(module_py_lines) > 0: + imports = '\n'.join(module_py_lines) + + template_content = stringTemplate(InitTemplate.get_init_py()).substitute( + Name=self._project_settings.name, + Description=self._project_settings.description, + LongDescription=self._project_settings.long_description, + CopyrightDate=self._project_settings.copyright_date, + CopyrightName=self._project_settings.copyright_name, + LicenseName=self._project_settings.license_name, + LicenseDescription=self._project_settings.license_description, + Title=title if title is not None and title != '' else self._project_settings.name, + Author=self._project_settings.author, + Version=version.parse(self._project_settings.version.to_str()), + Major=self._project_settings.version.major, + Minor=self._project_settings.version.minor, + Micro=self._project_settings.version.micro, + Imports=imports + ) + + with open(file, 'w+') as py_file: + py_file.write(template_content) + py_file.close() + + def _dist_files(self): + """ + Copies all included source files to dist_path + :return: + """ + build_path = os.path.join(self._output_path) + self._delete_path(build_path) + self._create_path(build_path) + + for file in self._included_files: + dist_file = file + if 'src/' in dist_file: + dist_file = dist_file.replace('src/', '', 1) + + output_path = os.path.join(build_path, os.path.dirname(dist_file)) + output_file = os.path.join(build_path, dist_file) + + try: + if not os.path.isdir(output_path): + os.makedirs(output_path, exist_ok=True) + except Exception as e: + Console.error(__name__, f'Cannot create directories: {output_path} -> {e}') + return + + try: + self._distributed_files.append(output_file) + shutil.copy(os.path.abspath(file), output_file) + except Exception as e: + Console.error(__name__, f'Cannot copy file: {file} to {output_path} -> {e}') + return + + for empty_dir in self._included_dirs: + dist_dir = empty_dir + if 'src/' in dist_dir: + dist_dir = dist_dir.replace('src/', '', 1) + + output_path = os.path.join(build_path, dist_dir) + if not os.path.isdir(output_path): + os.makedirs(output_path) + + def _clean_dist_files(self): + """ + Deletes all included source files from dist_path + :return: + """ + paths: list[str] = [] + for file in self._distributed_files: + paths.append(os.path.dirname(file)) + + if os.path.isfile(file): + os.remove(file) + + for path in paths: + if path != self._output_path and os.path.isdir(path): + shutil.rmtree(path) + + def _create_setup(self): + """ + Generates setup.py + + Dependencies: ProjectSettings, BuildSettings + :return: + """ + setup_file = os.path.join(self._output_path, 'setup.py') + if os.path.isfile(setup_file): + os.remove(setup_file) + + main_mod = None + try: + # main = importlib.import_module(self._build_settings.main) + file_name = f'{str(self._build_settings.main).replace(".", "/")}.py' + print() + spec = importlib.util.spec_from_file_location(self._build_settings.main, os.path.join(self._env.working_directory, 'src', file_name)) + print(1, spec) + test = importlib.util.module_from_spec(spec) + print(2, test) + # spec.loader.exec_module(test) + sys.path.append(os.path.join(self._env.working_directory, 'src')) + import main + print(3, main) + main_mod = main + except Exception as e: + Console.error('Could not find entry point', str(e)) + return + + if main_mod is None or not hasattr(main_mod, 'main'): + Console.error('Could not find entry point') + return + + with open(setup_file, 'w+') as setup_py: + setup_string = stringTemplate(SetupTemplate.get_setup_py()).substitute( + Name=self._project_settings.name, + Version=self._project_settings.version.to_str(), + Packages=setuptools.find_packages(where=self._output_path, exclude=self._build_settings.excluded), + URL=self._project_settings.url, + LicenseName=self._project_settings.license_name, + Author=self._project_settings.author, + AuthorMail=self._project_settings.author_email, + IncludePackageData=self._build_settings.include_package_data, + Description=self._project_settings.description, + PyRequires=self._project_settings.python_version, + Dependencies=self._project_settings.dependencies, + EntryPoints={ + 'console_scripts': [ + f'{self._build_settings.entry_point} = {main_mod.__name__}:{main_mod.main.__name__}' + ] + }, + PackageData=self._build_settings.package_data + ) + setup_py.write(setup_string) + setup_py.close() + + def _run_setup(self): + """ + Starts setup.py + :return: + """ + setup_py = os.path.join(self._output_path, 'setup.py') + if not os.path.isfile(setup_py): + Console.error(__name__, f'setup.py not found in {self._output_path}') + return + + try: + sandbox.run_setup(os.path.abspath(setup_py), [ + 'sdist', + f'--dist-dir={os.path.join(self._output_path, "setup")}', + 'bdist_wheel', + f'--bdist-dir={os.path.join(self._output_path, "bdist")}', + f'--dist-dir={os.path.join(self._output_path, "setup")}' + ]) + os.remove(setup_py) + except Exception as e: + Console.error('Executing setup.py failed', str(e)) + + def include(self, path: str): + """ + Includes given path from sources + :param path: + :return: + """ + self._build_settings.included.append(path) + + def exclude(self, path: str): + """ + Excludes given path from sources + :param path: + :return: + """ + self._build_settings.excluded.append(path) + + def build(self): + """ + Build the CPL project to dist_path/build + + 1. Reads all included source files + 2. Writes informations from template to all included __init__.py + 3. Copies all included source files to dist_path/build + :return: + """ + self._output_path = os.path.join(self._output_path, 'build') + + Console.spinner('Reading source files:', self._read_sources, text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue) + Console.spinner('Creating internal packages:', self._create_packages, + text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue) + Console.spinner('Building application:', self._dist_files, text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue) + + def publish(self): + """ + Publishes the CPL project to dist_path/publish + + 1. Builds the project + 2. Generates setup.py + 3. Start setup.py + 4. Remove all included source from dist_path/publish + :return: + """ + self._output_path = os.path.join(self._output_path, 'publish') + + Console.write_line('Build:') + Console.spinner('Reading source files:', self._read_sources, text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue) + Console.spinner('Creating internal packages:', self._create_packages, + text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue) + Console.spinner('Building application:', self._dist_files, text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue) + + Console.write_line('\nPublish:') + Console.spinner('Generating setup.py:', self._create_setup, text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue) + Console.write_line('Running setup.py:\n') + self._run_setup() + Console.spinner('Cleaning dist path:', self._clean_dist_files, text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.blue)