diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94bff63..dc53ff2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,22 +8,16 @@ jobs: fail-fast: false matrix: python-version: - - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" + - "3.13" os: - ubuntu-latest - macos-latest - windows-latest exclude: - # python 3.8 and windows do not like each other here, so we do not support it - - python-version: "3.8" - os: windows-latest - # GH does not support macos and python 3.8 - - python-version: "3.8" - os: macos-latest # GH does not support macos and python 3.9 - python-version: "3.9" os: macos-latest diff --git a/.github/workflows/variants.yml b/.github/workflows/variants.yml index 33a0e51..f60f4ed 100644 --- a/.github/workflows/variants.yml +++ b/.github/workflows/variants.yml @@ -9,16 +9,12 @@ jobs: matrix: python-version: # we test on lowest and highest supported versions - - "3.8" + - "3.9" - "3.12" os: - ubuntu-latest - macos-latest - windows-latest - exclude: - # python 3.8 and windows do not like each other here, so we do not support it - - python-version: "3.8" - os: windows-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/CHANGES.md b/CHANGES.md index 564c625..41c5ff0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,15 @@ # Changelog -## 1.0a6 (unreleased) +## 1.0a7 (unreleased) + +- Add proxy target support. + +**Breaking changes** + +- Rename `npm` domain to `nodejs` and add support for using `pnpm` as + alternative package manager. + +## 1.0a6 (2024-08-02) - Fix bug in `Template.write` when creating target folders to also create parent folders if not exists. @@ -9,10 +18,9 @@ - Add `plone-site` template configuration to `mx.ini` template. -**Breaking changes** +- More fine grained control over plone site creation and purging. -- Rename `npm` domain to `nodejs` and add support for using `pnpm` as - alternative package manager. +- Drop Python 3.8 and set all defaults to a Python 3.9 minimum. ## 1.0a5 (2024-06-07) diff --git a/Makefile b/Makefile index 6342d58..8b73f1f 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ PRIMARY_PYTHON?=python3 # Minimum required Python version. # Default: 3.7 -PYTHON_MIN_VERSION?=3.7 +PYTHON_MIN_VERSION?=3.9 # Install packages using the given package installer method. # Supported are `pip` and `uv`. If uv is used, its global availability is diff --git a/pyproject.toml b/pyproject.toml index 963a97b..6678850 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "mxmake" description = "Generates a Python project-specific Makefile by using an extensible library of configurable Makefile snippets." -version = "1.0a6.dev0" +version = "1.0a7.dev0" keywords = ["development", "deployment", "make"] authors = [ {name = "MX Stack Developers", email = "dev@bluedynamics.com" } ] -requires-python = ">=3.7" +requires-python = ">=3.9" license = { text = "BSD 2-Clause License" } classifiers = [ "Development Status :: 3 - Alpha", diff --git a/src/mxmake/main.py b/src/mxmake/main.py index 3f6a16e..4c9cd51 100644 --- a/src/mxmake/main.py +++ b/src/mxmake/main.py @@ -199,9 +199,8 @@ def create_config( preseed_value = ( preseed_domain.get(setting.name, unset) if preseed_domain else unset ) - setting_default = ( - preseed_value if preseed_value is not unset else setting_default - ) + if preseed_value is unset: + preseed_value = setting_default # use configured setting from parser if set elif sfqn in parser.settings: setting_default = parser.settings[sfqn] diff --git a/src/mxmake/templates.py b/src/mxmake/templates.py index 6226334..deb67ac 100644 --- a/src/mxmake/templates.py +++ b/src/mxmake/templates.py @@ -1,6 +1,7 @@ from jinja2 import Environment from jinja2 import PackageLoader from mxmake.topics import Domain +from mxmake.topics import get_topic from mxmake.topics import load_topics from mxmake.utils import gh_actions_path from mxmake.utils import mxmake_files @@ -277,7 +278,6 @@ def template_variables(self) -> typing.Dict[str, typing.Any]: additional_targets = {} topics = {domain.topic for domain in self.domains} additional_targets["qa"] = "qa" in topics - # additional_targets["docs"] = "docs" in topics # return template variables return dict( settings=settings, @@ -525,3 +525,42 @@ def template_variables(self): if not site["extension_ids"]: site["extension_ids"] = ["plone.volto:default"] return vars + + +############################################################################## +# proxy targets template +############################################################################## + + +@template("proxy") +class ProxyMk(MxIniBoundTemplate): + description: str = "Contains proxy targets for Makefiles of source folders" + target_name = "proxy.mk" + template_name = "proxy.mk" + + @property + def target_folder(self) -> Path: + return mxmake_files() + + @property + def template_variables(self): + targets = [] + for folder, proxy in self.settings.items(): + for item in [item.strip() for item in proxy.split('\n') if item.strip()]: + topic_name, domain_names = item.split(':') + topic = get_topic(topic_name.strip()) + domain_names = domain_names.split(',') + domains = [] + for domain_name in domain_names: + if domain_name == '*': + domains = topic.domains + break + else: + domains.append(topic.domain(domain_name.strip())) + for domain in domains: + for target in domain.targets: + targets.append(dict( + name=target.name, + folder=folder + )) + return dict(targets=targets) diff --git a/src/mxmake/templates/Makefile b/src/mxmake/templates/Makefile index 5136b99..733b3ec 100644 --- a/src/mxmake/templates/Makefile +++ b/src/mxmake/templates/Makefile @@ -39,6 +39,10 @@ TYPECHECK_TARGETS?= FORMAT_TARGETS?= {% endif %} {{ sections.read() }} +############################################################################## +# Custom includes +############################################################################## + -include $(INCLUDE_MAKEFILE) ############################################################################## diff --git a/src/mxmake/templates/plone-site.py b/src/mxmake/templates/plone-site.py index 7d8278a..617739f 100644 --- a/src/mxmake/templates/plone-site.py +++ b/src/mxmake/templates/plone-site.py @@ -10,7 +10,7 @@ TRUTHY = frozenset(("t", "true", "y", "yes", "on", "1")) -def asbool(value: str|bool|None) -> bool: +def asbool(value: str | bool | None) -> bool: """Return the boolean value ``True`` if the case-lowered value of string input ``s`` is a :term:`truthy string`. If ``s`` is already one of the boolean values ``True`` or ``False``, return it. @@ -22,7 +22,14 @@ def asbool(value: str|bool|None) -> bool: return value.strip().lower() in TRUTHY -PLONE_SITE_PURGE = asbool(os.getenv("PLONE_SITE_PURGE")) +PLONE_SITE_PURGE = asbool(os.getenv("PLONE_SITE_PURGE", "false")) +PLONE_SITE_PURGE_FAIL_IF_NOT_EXISTS = asbool( + os.getenv("PLONE_SITE_PURGE_FAIL_IF_NOT_EXISTS", "true") +) +PLONE_SITE_CREATE = asbool(os.getenv("PLONE_SITE_CREATE", "true")) +PLONE_SITE_CREATE_FAIL_IF_EXISTS = asbool( + os.getenv("PLONE_SITE_CREATE_FAIL_IF_EXISTS", "true") +) config = { {% for key, value in site.items() %} @@ -49,15 +56,30 @@ def asbool(value: str|bool|None) -> bool: app.manage_delObjects([config["site_id"]]) transaction.commit() app._p_jar.sync() + print(f"Existing site with id={config['site_id']} purged!") + if not PLONE_SITE_CREATE: + print("Done.") + exit(0) else: - print(f"Site with id {config['site_id']} does not exist!") - exit(0) + print(f"Site with id={config['site_id']} does not exist!") + if PLONE_SITE_PURGE_FAIL_IF_NOT_EXISTS: + print("...failure!") + exit(1) + if not PLONE_SITE_CREATE: + print("Done.") + exit(0) +if PLONE_SITE_CREATE: + if config["site_id"] in app.objectIds(): + print(f"Site with id={config['site_id']} already exists!") + if PLONE_SITE_CREATE_FAIL_IF_EXISTS: + print("...failure!") + exit(1) + print("Done.") + exit(0) -if config["site_id"] in app.objectIds(): - print(f"Site with id {config['site_id']} already exists!") - exit(1) - -site = create(app, "{{ distribution }}", config) -transaction.commit() -app._p_jar.sync() + site = create(app, "{{ distribution }}", config) + transaction.commit() + app._p_jar.sync() + print(f"New site with id={config['site_id']} created!") + print("Done.") diff --git a/src/mxmake/templates/proxy.mk b/src/mxmake/templates/proxy.mk new file mode 100644 index 0000000..b1b36d8 --- /dev/null +++ b/src/mxmake/templates/proxy.mk @@ -0,0 +1,10 @@ +############################################################################## +# proxy targets +############################################################################## + +{% for target in targets %} +.PHONY: {{ target["folder"] }}-{{ target["name"] }} +{{ target["folder"] }}-{{ target["name"] }}: + $(MAKE) -C "./{{ target["folder"] }}/" {{ target["name"] }} + +{% endfor %} \ No newline at end of file diff --git a/src/mxmake/tests/test_templates.py b/src/mxmake/tests/test_templates.py index 32e27ab..fe1e213 100644 --- a/src/mxmake/tests/test_templates.py +++ b/src/mxmake/tests/test_templates.py @@ -32,6 +32,7 @@ class Template(templates.Template): "makefile": templates.Makefile, "mx.ini": templates.MxIni, "pip-conf": templates.PipConf, + "proxy": templates.ProxyMk, "run-coverage": templates.CoverageScript, "run-tests": templates.TestScript, "topics.md": templates.Topics, @@ -524,7 +525,7 @@ def test_Makefile(self, tempdir): "core.base.INCLUDE_MAKEFILE": "include.mk", "core.base.EXTRA_PATH": "", "core.mxenv.PRIMARY_PYTHON": "python3", - "core.mxenv.PYTHON_MIN_VERSION": "3.7", + "core.mxenv.PYTHON_MIN_VERSION": "3.9", "core.mxenv.PYTHON_PACKAGE_INSTALLER": "pip", "core.mxenv.MXENV_UV_GLOBAL": "false", "core.mxenv.VENV_ENABLED": "true", @@ -585,8 +586,8 @@ def test_Makefile(self, tempdir): PRIMARY_PYTHON?=python3 # Minimum required Python version. - # Default: 3.7 - PYTHON_MIN_VERSION?=3.7 + # Default: 3.9 + PYTHON_MIN_VERSION?=3.9 # Install packages using the given package installer method. # Supported are `pip` and `uv`. If uv is used, its global availability is @@ -740,6 +741,10 @@ def test_Makefile(self, tempdir): DIRTY_TARGETS+=mxenv-dirty CLEAN_TARGETS+=mxenv-clean + ############################################################################## + # Custom includes + ############################################################################## + -include $(INCLUDE_MAKEFILE) ############################################################################## @@ -830,13 +835,6 @@ def test_MxIni(self, tempdir): @testing.template_directory() def test_PloneSite_all_defaults(self, tempdir): mxini = tempdir / "mx.ini" - with mxini.open("w") as fd: - fd.write( - "[settings]\n" - "\n" - "[mxmake-plone-site]\n" - "distribution = mxmake.test:default\n" - ) with mxini.open("w") as fd: fd.write("[settings]\n") configuration = mxdev.Configuration(mxini, hooks=[hook.Hook()]) @@ -877,7 +875,7 @@ def test_PloneSite_all_defaults(self, tempdir): TRUTHY = frozenset(("t", "true", "y", "yes", "on", "1")) - def asbool(value: str|bool|None) -> bool: + def asbool(value: str | bool | None) -> bool: """Return the boolean value ``True`` if the case-lowered value of string input ``s`` is a :term:`truthy string`. If ``s`` is already one of the boolean values ``True`` or ``False``, return it. @@ -889,7 +887,14 @@ def asbool(value: str|bool|None) -> bool: return value.strip().lower() in TRUTHY - PLONE_SITE_PURGE = asbool(os.getenv("PLONE_SITE_PURGE")) + PLONE_SITE_PURGE = asbool(os.getenv("PLONE_SITE_PURGE", "false")) + PLONE_SITE_PURGE_FAIL_IF_NOT_EXISTS = asbool( + os.getenv("PLONE_SITE_PURGE_FAIL_IF_NOT_EXISTS", "true") + ) + PLONE_SITE_CREATE = asbool(os.getenv("PLONE_SITE_CREATE", "true")) + PLONE_SITE_CREATE_FAIL_IF_EXISTS = asbool( + os.getenv("PLONE_SITE_CREATE_FAIL_IF_EXISTS", "true") + ) config = { "site_id": "Plone", @@ -913,18 +918,111 @@ def asbool(value: str|bool|None) -> bool: app.manage_delObjects([config["site_id"]]) transaction.commit() app._p_jar.sync() + print(f"Existing site with id={config['site_id']} purged!") + if not PLONE_SITE_CREATE: + print("Done.") + exit(0) else: - print(f"Site with id {config['site_id']} does not exist!") - exit(0) + print(f"Site with id={config['site_id']} does not exist!") + if PLONE_SITE_PURGE_FAIL_IF_NOT_EXISTS: + print("...failure!") + exit(1) + if not PLONE_SITE_CREATE: + print("Done.") + exit(0) + + if PLONE_SITE_CREATE: + if config["site_id"] in app.objectIds(): + print(f"Site with id={config['site_id']} already exists!") + if PLONE_SITE_CREATE_FAIL_IF_EXISTS: + print("...failure!") + exit(1) + print("Done.") + exit(0) + + site = create(app, "", config) + transaction.commit() + app._p_jar.sync() + print(f"New site with id={config['site_id']} created!") + print("Done.") + ''', + f.read(), + ) + + @testing.template_directory() + def test_ProxyMk(self, tempdir): + mxini = tempdir / "mx.ini" + with mxini.open("w") as fd: + fd.write( + "[settings]\n" + "\n" + "[mxmake-proxy]\n" + "folder =\n" + " applications:plone\n" + " i18n:*\n" + ) + configuration = mxdev.Configuration(mxini, hooks=[hook.Hook()]) + factory = templates.template.lookup("proxy") + template = factory(configuration, templates.get_template_environment()) + + self.assertEqual(template.description, "Contains proxy targets for Makefiles of source folders") + self.assertEqual(template.target_folder, utils.mxmake_files()) + self.assertEqual(template.target_name, "proxy.mk") + self.assertEqual(template.template_name, "proxy.mk") + self.assertEqual( + template.template_variables, + {'targets': [ + {'name': 'plone-site-create', 'folder': 'folder'}, + {'name': 'plone-site-purge', 'folder': 'folder'}, + {'name': 'plone-site-recreate', 'folder': 'folder'}, + {'name': 'gettext-create', 'folder': 'folder'}, + {'name': 'gettext-update', 'folder': 'folder'}, + {'name': 'gettext-compile', 'folder': 'folder'}, + {'name': 'lingua-extract', 'folder': 'folder'}, + {'name': 'lingua', 'folder': 'folder'} + ]} + ) + + template.write() + with (tempdir / "proxy.mk").open() as f: + self.checkOutput( + ''' + ############################################################################## + # proxy targets + ############################################################################## + + .PHONY: folder-plone-site-create + folder-plone-site-create: + $(MAKE) -C "./folder/" plone-site-create + + .PHONY: folder-plone-site-purge + folder-plone-site-purge: + $(MAKE) -C "./folder/" plone-site-purge + + .PHONY: folder-plone-site-recreate + folder-plone-site-recreate: + $(MAKE) -C "./folder/" plone-site-recreate + + .PHONY: folder-gettext-create + folder-gettext-create: + $(MAKE) -C "./folder/" gettext-create + + .PHONY: folder-gettext-update + folder-gettext-update: + $(MAKE) -C "./folder/" gettext-update + + .PHONY: folder-gettext-compile + folder-gettext-compile: + $(MAKE) -C "./folder/" gettext-compile + .PHONY: folder-lingua-extract + folder-lingua-extract: + $(MAKE) -C "./folder/" lingua-extract - if config["site_id"] in app.objectIds(): - print(f"Site with id {config['site_id']} already exists!") - exit(1) + .PHONY: folder-lingua + folder-lingua: + $(MAKE) -C "./folder/" lingua - site = create(app, "", config) - transaction.commit() - app._p_jar.sync() ''', f.read(), ) diff --git a/src/mxmake/topics/applications/plone.mk b/src/mxmake/topics/applications/plone.mk index 03bf7e4..42531f0 100644 --- a/src/mxmake/topics/applications/plone.mk +++ b/src/mxmake/topics/applications/plone.mk @@ -4,14 +4,28 @@ #:depends = applications.zope #: #:[target.plone-site-create] -#:description = Creates a Plone site using the script provided in `PLONE_SITE_SCRIPT` configuration. +#:description = Creates a Plone site using the script provided in +#: `PLONE_SITE_SCRIPT` configuration. #: #:[target.plone-site-purge] -#:description = Removes the Plone instance from the database, but the database itself is kept. +#:description = Removes the Plone instance from the database, but the database +#: itself is kept. +#: +#:[target.plone-site-recreate] +#:description = Removes the Plone instance from the database like in plone-site-purge, +#: then creates a new one like in plone-site-create. #: #:[setting.PLONE_SITE_SCRIPT] #:description = Path to the script to create or purge a Plone site #:default = .mxmake/files/plone-site.py +#:#: +#:[setting.PLONE_SITE_CREATE_FAIL_IF_EXISTS] +#:description = Exit with an error if the Plone site already exists +#:default = True + +#:[setting.PLONE_SITE_PURGE_FAIL_IF_NOT_EXISTS] +#:description = Exit with an error if the Plone site does not exists +#:default = True ############################################################################## # plone @@ -20,10 +34,20 @@ .PHONY: plone-site-create plone-site-create: $(ZOPE_RUN_TARGET) @echo "Creating Plone Site" + @export PLONE_SITE_PURGE=False + @export PLONE_SITE_CREATE=True @zconsole run $(ZOPE_INSTANCE_FOLDER)/etc/zope.conf $(PLONE_SITE_SCRIPT) .PHONY: plone-site-purge plone-site-purge: $(ZOPE_RUN_TARGET) @echo "Purging Plone Site" @export PLONE_SITE_PURGE=True + @export PLONE_SITE_CREATE=False + @zconsole run $(ZOPE_INSTANCE_FOLDER)/etc/zope.conf $(PLONE_SITE_SCRIPT) + +.PHONY: plone-site-recreate +plone-site-recreate: $(ZOPE_RUN_TARGET) + @echo "Purging Plone Site" + @export PLONE_SITE_PURGE=True + @export PLONE_SITE_CREATE=True @zconsole run $(ZOPE_INSTANCE_FOLDER)/etc/zope.conf $(PLONE_SITE_SCRIPT) diff --git a/src/mxmake/topics/core/mxenv.mk b/src/mxmake/topics/core/mxenv.mk index 3157388..c352fcf 100644 --- a/src/mxmake/topics/core/mxenv.mk +++ b/src/mxmake/topics/core/mxenv.mk @@ -28,7 +28,7 @@ #: #:[setting.PYTHON_MIN_VERSION] #:description = Minimum required Python version. -#:default = 3.7 +#:default = 3.9 #: #:[setting.PYTHON_PACKAGE_INSTALLER] #:description = Install packages using the given package installer method. diff --git a/src/mxmake/topics/core/proxy.mk b/src/mxmake/topics/core/proxy.mk new file mode 100644 index 0000000..344760e --- /dev/null +++ b/src/mxmake/topics/core/proxy.mk @@ -0,0 +1,29 @@ +#:[proxy] +#:title = Proxy targets +#:description = This domain includes proxy targets which are configured in +#: `mx.ini`. It is expected that defined folder(s) contains a Makefile which +#: is generated by `mxmake` and this Makefile contains the domains for which +#: proxy targets are created. The proxy configuration in the `mx.ini` file +#: looks as follows: +#: +#: ```ini +#: mxmake-templates = proxy +#: +#: [mxmake-proxy] +#: foldername = +#: applications:plone,zest-releaser +#: i18n:* +#: ``` +#: +#: Each setting in the `mxmake-proxy` section defines a child folder. The +#: value contains the topic name and the desired domains as comma +#: separated list. Wildcard `*` means to include all domains of this topic. +#: Topic and domains are colon separated. +#: +#:depends = core.mxfiles + +############################################################################## +# proxy +############################################################################## + +-include $(MXMAKE_FILES)/proxy.mk