Переглянути джерело

feat: add git init option to templates

trgiangdo 11 місяців тому
батько
коміт
b6ee58a13c

+ 3 - 1
taipy/templates/default/cookiecutter.json

@@ -5,11 +5,13 @@
     "Page names in multi-page application?": "",
     "Does the application use scenario management or version management?": "No",
     "Does the application use Rest API?": "No",
+    "Do you want to initialize a new Git repository?": "No",
 
     "__root_folder_name": "{{ cookiecutter['Application root folder name'] | replace(' ','-') }}",
     "__main_file": "{{ cookiecutter['Application main Python file'][:-3] if cookiecutter['Application main Python file'].endswith('.py') else cookiecutter['Application main Python file'] }}",
     "__application_title": "{{ cookiecutter['Application title'] }}",
     "__pages": "{{ cookiecutter['Page names in multi-page application?'] }}",
     "__core": "{{ cookiecutter['Does the application use scenario management or version management?'] }}",
-    "__rest": "{{ cookiecutter['Does the application use Rest API?'] }}"
+    "__rest": "{{ cookiecutter['Does the application use Rest API?'] }}",
+    "__git": "{{ cookiecutter['Do you want to initialize a new Git repository?']}}"
 }

+ 28 - 0
taipy/templates/default/hooks/post_gen_project.py

@@ -11,6 +11,7 @@
 
 import os
 import shutil
+import subprocess
 
 
 def handle_services(use_rest, use_core):
@@ -149,6 +150,27 @@ def generate_main_file():
         app_main_file.write(main_lines)
 
 
+def initialize_as_git_project(project_dir: str) -> str:
+    if shutil.which("git") is None:
+        msg = "\nERROR: Git executable not found, skipping git initialisation"
+        return msg
+
+    try:
+        subprocess.run(
+            ["git", "init", "."],
+            cwd=project_dir,
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+            check=True,
+        )
+        msg = f"\nInitialized Git repository in {project_dir}"
+
+    except subprocess.CalledProcessError:
+        msg = f"\nERROR: Failed to initialise Git repository in {project_dir}"
+
+    return msg
+
+
 use_core = "{{ cookiecutter.__core }}".upper()
 use_rest = "{{ cookiecutter.__rest }}".upper()
 handle_services(use_rest in ["YES", "Y"], use_core in ["YES", "Y"])
@@ -166,9 +188,15 @@ generate_main_file()
 # Remove the sections folder
 shutil.rmtree(os.path.join(os.getcwd(), "sections"))
 
+# Initialize the project as a git repository
+git_init_message = ""
+if "{{ cookiecutter.__git }}".upper() in ["YES", "Y"]:
+    git_init_message = initialize_as_git_project(os.getcwd())
+
 main_file_name = "{{cookiecutter.__main_file}}.py"
 print(
     f"New Taipy application has been created at {os.path.join(os.getcwd())}"
+    f"{git_init_message}"
     f"\n\nTo start the application, change directory to the newly created folder:"
     f"\n\tcd {os.path.join(os.getcwd())}"
     f"\nand run the application as follows:"

+ 166 - 0
taipy/templates/default/{{cookiecutter.__root_folder_name}}/.gitignore

@@ -0,0 +1,166 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Taipy
+.taipy/
+user_data/

+ 3 - 1
taipy/templates/scenario-management/cookiecutter.json

@@ -3,9 +3,11 @@
     "Application main Python file": "main.py",
     "Application title": "Taipy Application",
     "Does the application use TOML Config?": "No",
+    "Do you want to initialize a new Git repository?": "No",
 
     "__root_folder_name": "{{ cookiecutter['Application root folder name'] | replace(' ','-') }}",
     "__main_file": "{{ cookiecutter['Application main Python file'][:-3] if cookiecutter['Application main Python file'].endswith('.py') else cookiecutter['Application main Python file'] }}",
     "__application_title": "{{ cookiecutter['Application title'] }}",
-    "__use_toml_config": "{{ cookiecutter['Does the application use TOML Config?'] }}"
+    "__use_toml_config": "{{ cookiecutter['Does the application use TOML Config?'] }}",
+    "__git": "{{ cookiecutter['Do you want to initialize a new Git repository?']}}"
 }

+ 30 - 0
taipy/templates/scenario-management/hooks/post_gen_project.py

@@ -10,6 +10,30 @@
 # specific language governing permissions and limitations under the License.
 
 import os
+import shutil
+import subprocess
+
+
+def initialize_as_git_project(project_dir: str) -> str:
+    if shutil.which("git") is None:
+        msg = "\nERROR: Git executable not found, skipping git initialisation"
+        return msg
+
+    try:
+        subprocess.run(
+            ["git", "init", "."],
+            cwd=project_dir,
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+            check=True,
+        )
+        msg = f"\nInitialized Git repository in {project_dir}"
+
+    except subprocess.CalledProcessError:
+        msg = f"\nERROR: Failed to initialise Git repository in {project_dir}"
+
+    return msg
+
 
 # Use TOML config file or not
 use_toml_config = "{{ cookiecutter.__use_toml_config }}".upper()
@@ -22,9 +46,15 @@ else:
     os.remove(os.path.join(os.getcwd(), "config", "config_with_toml.py"))
     os.remove(os.path.join(os.getcwd(), "config", "config.toml"))
 
+# Initialize the project as a git repository
+git_init_message = ""
+if "{{ cookiecutter.__git }}".upper() in ["YES", "Y"]:
+    git_init_message = initialize_as_git_project(os.getcwd())
+
 main_file_name = "{{cookiecutter.__main_file}}.py"
 print(
     f"New Taipy application has been created at {os.path.join(os.getcwd())}"
+    f"{git_init_message}"
     f"\n\nTo start the application, change directory to the newly created folder:"
     f"\n\tcd {os.path.join(os.getcwd())}"
     f"\nand run the application as follows:"

+ 164 - 2
taipy/templates/scenario-management/{{cookiecutter.__root_folder_name}}/.gitignore

@@ -1,4 +1,166 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
 # Taipy
-.data/
-user_data/
 .taipy/
+user_data/

+ 32 - 18
tests/templates/test_default_template.py

@@ -26,8 +26,8 @@ def test_default_answer(tmpdir):
     )
 
     assert os.listdir(tmpdir) == ["taipy_application"]
-    assert (
-        os.listdir(os.path.join(tmpdir, "taipy_application")).sort() == ["requirements.txt", "main.py", "images"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "taipy_application"))) == sorted(
+        ["requirements.txt", "main.py", "images"]
     )
 
     taipy_path = os.getcwd()
@@ -46,8 +46,8 @@ def test_main_file_with_and_without_extension(tmpdir):
             "Application main Python file": "app.py",
         },
     )
-    assert (
-        os.listdir(os.path.join(tmpdir, "taipy_application")).sort() == ["requirements.txt", "app.py", "images"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "taipy_application"))) == sorted(
+        ["requirements.txt", "app.py", "images"]
     )
 
     cookiecutter(
@@ -59,7 +59,7 @@ def test_main_file_with_and_without_extension(tmpdir):
             "Application main Python file": "app",
         },
     )
-    assert os.listdir(os.path.join(tmpdir, "foo_app")).sort() == ["requirements.txt", "app.py", "images"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app"))) == sorted(["requirements.txt", "app.py", "images"])
 
 
 def test_with_core_service(tmpdir):
@@ -73,9 +73,8 @@ def test_with_core_service(tmpdir):
         },
     )
 
-    assert (
-        os.listdir(os.path.join(tmpdir, "taipy_application")).sort()
-        == ["requirements.txt", "main.py", "images", "configuration", "algorithms"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "taipy_application"))) == sorted(
+        ["requirements.txt", "main.py", "images", "configuration", "algorithms"]
     )
     with open(os.path.join(tmpdir, "taipy_application", "main.py")) as main_file:
         assert "core = Core()" in main_file.read()
@@ -99,8 +98,8 @@ def test_with_rest_service(tmpdir):
         },
     )
 
-    assert (
-        os.listdir(os.path.join(tmpdir, "taipy_application")).sort() == ["requirements.txt", "main.py", "images"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "taipy_application"))) == sorted(
+        ["requirements.txt", "main.py", "images"]
     )
     with open(os.path.join(tmpdir, "taipy_application", "main.py")) as main_file:
         assert "rest = Rest()" in main_file.read()
@@ -124,9 +123,8 @@ def test_with_both_core_rest_services(tmpdir):
         },
     )
 
-    assert (
-        os.listdir(os.path.join(tmpdir, "taipy_application")).sort()
-        == ["requirements.txt", "main.py", "images", "configuration", "algorithms"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "taipy_application"))) == sorted(
+        ["requirements.txt", "main.py", "images", "configuration", "algorithms"]
     )
     with open(os.path.join(tmpdir, "taipy_application", "main.py")) as main_file:
         assert "rest = Rest()" in main_file.read()
@@ -151,12 +149,11 @@ def test_multipage_gui_template(tmpdir):
         },
     )
 
-    assert (
-        os.listdir(os.path.join(tmpdir, "foo_app")).sort() == ["requirements.txt", "main.py", "pages", "images"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app"))) == sorted(
+        ["requirements.txt", "main.py", "pages", "images"]
     )
-    assert (
-        os.listdir(os.path.join(tmpdir, "foo_app", "pages")).sort()
-        == ["name_1", "name_2", "name_3", "root.md", "root.py", "__init__.py"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app", "pages"))) == sorted(
+        ["name_1", "name_2", "name_3", "root.md", "root.py", "__init__.py"]
     )
 
     taipy_path = os.getcwd()
@@ -180,3 +177,20 @@ def test_multipage_gui_template_with_invalid_page_name(tmpdir, capfd):
     assert 'Page name "1_invalid_var_name" is not a valid Python identifier' in stderr
 
     assert not os.path.exists(os.path.join(tmpdir, "foo_app"))
+
+
+def test_with_git(tmpdir):
+    cookiecutter(
+        template="taipy/templates/default",
+        output_dir=str(tmpdir),
+        no_input=True,
+        extra_context={
+            "Application root folder name": "foo_app",
+            "Do you want to initialize a new Git repository?": "y",
+        },
+    )
+
+    assert os.listdir(tmpdir) == ["foo_app"]
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app"))) == sorted(
+        ["requirements.txt", "main.py", ".git", ".gitignore"]
+    )

+ 24 - 10
tests/templates/test_scenario_mgt_template.py

@@ -30,14 +30,12 @@ def test_scenario_management_with_toml_config(tmpdir):
     )
 
     assert os.listdir(tmpdir) == ["foo_app"]
-    assert (
-        os.listdir(os.path.join(tmpdir, "foo_app")).sort()
-        == ["requirements.txt", "main.py", "algos", "config", "pages"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app"))) == sorted(
+        ["requirements.txt", "main.py", "algos", "config", "pages"]
     )
 
-    assert (
-        os.listdir(os.path.join(tmpdir, "foo_app", "config")).sort()
-        == ["__init__.py", "config.py", "config.toml"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app", "config"))) == sorted(
+        ["__init__.py", "config.py", "config.toml"]
     )
     with open(os.path.join(tmpdir, "foo_app", "config", "config.py")) as config_file:
         assert 'Config.load("config/config.toml")' in config_file.read()
@@ -64,12 +62,11 @@ def test_scenario_management_without_toml_config(tmpdir):
     )
 
     assert os.listdir(tmpdir) == ["foo_app"]
-    assert (
-        os.listdir(os.path.join(tmpdir, "foo_app")).sort()
-        == ["requirements.txt", "main.py", "algos", "config", "pages"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app"))) == sorted(
+        ["requirements.txt", "main.py", "algos", "config", "pages"]
     )
 
-    assert os.listdir(os.path.join(tmpdir, "foo_app", "config")).sort() == ["__init__.py", "config.py"].sort()
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app", "config"))) == sorted(["__init__.py", "config.py"])
     with open(os.path.join(tmpdir, "foo_app", "config", "config.py")) as config_file:
         config_content = config_file.read()
         assert 'Config.load("config/config.toml")' not in config_content
@@ -80,3 +77,20 @@ def test_scenario_management_without_toml_config(tmpdir):
 
     # Assert the message when the application is run successfully is in the stdout
     assert "[Taipy][INFO]  * Server starting on" in stdout
+
+
+def test_with_git(tmpdir):
+    cookiecutter(
+        template="taipy/templates/scenario-management",
+        output_dir=str(tmpdir),
+        no_input=True,
+        extra_context={
+            "Application root folder name": "foo_app",
+            "Do you want to initialize a new Git repository?": "y",
+        },
+    )
+
+    assert os.listdir(tmpdir) == ["foo_app"]
+    assert sorted(os.listdir(os.path.join(tmpdir, "foo_app"))) == sorted(
+        ["requirements.txt", "main.py", ".git", ".gitignore", ".taipyignore", "algos", "config", "pages"]
+    )