From cab0d3415cf4662b86e1d943fa80950b200dae7b Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 27 Feb 2024 16:00:34 +0100 Subject: [PATCH] use pathlib --- CHANGES.md | 2 ++ src/mxmake/hook.py | 14 ++++----- src/mxmake/main.py | 8 ++--- src/mxmake/parser.py | 8 ++--- src/mxmake/templates.py | 43 +++++++++++++------------- src/mxmake/testing/__init__.py | 9 +++--- src/mxmake/tests/test_hook.py | 7 ++--- src/mxmake/tests/test_parser.py | 3 +- src/mxmake/tests/test_templates.py | 40 ++++++++++++------------- src/mxmake/tests/test_topics.py | 23 +++++++------- src/mxmake/tests/test_utils.py | 9 +++--- src/mxmake/topics.py | 48 ++++++++++++++---------------- src/mxmake/utils.py | 15 ++++++---- 13 files changed, 115 insertions(+), 114 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0dcada80..22350889 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,8 @@ - Drop Python 3.7 support. +- Use `pathlib.Path` instead of `os.path`. + ## 1.0a3 (2024-02-06) - Add `typecheck` target and use it for mypy instead of `check` target. diff --git a/src/mxmake/hook.py b/src/mxmake/hook.py index dc77999a..55e0c6ae 100644 --- a/src/mxmake/hook.py +++ b/src/mxmake/hook.py @@ -3,10 +3,10 @@ from mxmake.utils import list_value from mxmake.utils import NAMESPACE from mxmake.utils import ns_name +from pathlib import Path import logging import mxdev -import os logger = logging.getLogger("mxmake") @@ -46,17 +46,15 @@ def generate_templates(self, state: mxdev.State): def generate_additional_sources_targets(self, state: mxdev.State): config = state.configuration additional_sources_targets = [] - sources_folder = config.settings.get("default-target", "sources") + sources_folder = Path(config.settings.get("default-target", "sources")) for package_name in config.packages: - source_folder = os.path.join(sources_folder, package_name) + source_folder = sources_folder / package_name # case new source package has been added to mx.ini - if not os.path.exists(source_folder): + if not source_folder.exists(): continue - for child in os.listdir(source_folder): + for child in source_folder.iterdir(): if child in ADDITIONAL_SOURCES_TARGETS: - additional_sources_targets.append( - os.path.join(source_folder, child) - ) + additional_sources_targets.append(source_folder / child) if not additional_sources_targets: return environment = get_template_environment() diff --git a/src/mxmake/main.py b/src/mxmake/main.py index f95d2965..8b57334f 100644 --- a/src/mxmake/main.py +++ b/src/mxmake/main.py @@ -9,13 +9,13 @@ from mxmake.topics import resolve_domain_dependencies from mxmake.topics import set_domain_runtime_depends from operator import attrgetter +from pathlib import Path from textwrap import indent import argparse import inquirer import logging import mxdev -import os import sys @@ -99,10 +99,10 @@ def init_command(args: argparse.Namespace): print("#######################\n") # obtain target folder - target_folder = os.getcwd() + target_folder = Path.cwd() # parse existing makefile - parser = MakefileParser(os.path.join(target_folder, "Makefile")) + parser = MakefileParser(target_folder / "Makefile") # obtain topics to include topics = load_topics() @@ -186,7 +186,7 @@ def init_command(args: argparse.Namespace): print("Skip generation of Makefile, nothing selected") # mx ini generation - if not os.path.exists(os.path.join(target_folder, "mx.ini")): + if not (target_folder / "mx.ini").exists(): print("\n``mx.ini`` configuration file not exists. Create One?") yn = inquirer.text(message="Y/n") if yn not in ["n", "N"]: diff --git a/src/mxmake/parser.py b/src/mxmake/parser.py index 600cde9d..be28796b 100644 --- a/src/mxmake/parser.py +++ b/src/mxmake/parser.py @@ -1,6 +1,6 @@ from mxmake.topics import get_domain +from pathlib import Path -import os import typing @@ -11,7 +11,7 @@ class SettingMissing(Exception): class MakefileParser: - def __init__(self, path: str): + def __init__(self, path: Path): self.path = path self.fqns: typing.List = [] self.topics: typing.Dict = {} @@ -61,9 +61,9 @@ def parse_setting(self, lines: typing.List[str], name: str) -> str: return value def parse(self) -> None: - if not os.path.exists(self.path): + if not self.path.exists(): return - with open(self.path) as fd: + with self.path.open() as fd: lines = [line.rstrip() for line in fd.readlines() if line.strip()] self.parse_fqns(lines) self.parse_settings(lines) diff --git a/src/mxmake/templates.py b/src/mxmake/templates.py index df2c2e40..f8965157 100644 --- a/src/mxmake/templates.py +++ b/src/mxmake/templates.py @@ -5,11 +5,11 @@ from mxmake.utils import gh_actions_path from mxmake.utils import mxmake_files from mxmake.utils import ns_name +from pathlib import Path import abc import io import mxdev -import os import typing @@ -58,7 +58,7 @@ def __init__( self.environment = environment @abc.abstractproperty - def target_folder(self) -> str: + def target_folder(self) -> Path: """Target folder for rendered template.""" @abc.abstractproperty @@ -78,20 +78,21 @@ def write(self) -> None: if not self.environment: raise RuntimeError("Cannot write template without environment") target_folder = self.target_folder - os.makedirs(target_folder, exist_ok=True) - target_path = os.path.join(target_folder, self.target_name) + target_folder.mkdir(exist_ok=True) + target_path = target_folder / self.target_name template = self.environment.get_template(self.template_name) - with open(target_path, "w") as f: + with target_path.open("w") as f: f.write(template.render(**self.template_variables)) - os.chmod(target_path, self.file_mode) + target_path.chmod(self.file_mode) def remove(self) -> bool: """Remove rendered template if exists. Return bool if file existed.""" - target_path = os.path.join(self.target_folder, self.target_name) - if os.path.exists(target_path): - os.remove(target_path) - return True - return False + target_path = self.target_folder / self.target_name + try: + target_path.unlink() + except FileNotFoundError: + return False + return True class MxIniBoundTemplate(Template): @@ -141,7 +142,7 @@ def test_runner(self): return self.config.settings.get("mxmake-test-runner", "pytest") @property - def target_folder(self) -> str: + def target_folder(self) -> Path: return mxmake_files() @property @@ -208,7 +209,7 @@ class PipConf(MxIniBoundTemplate): template_name: str = "pip.conf" @property - def target_folder(self) -> str: + def target_folder(self) -> Path: return mxmake_files() @property @@ -232,11 +233,11 @@ class Makefile(Template): description: str = "Makefile" target_name = "Makefile" template_name = "Makefile" - target_folder = "" + target_folder = Path() def __init__( self, - target_folder: str, + target_folder: Path, domains: typing.List[Domain], domain_settings: typing.Dict[str, str], environment: typing.Union[Environment, None] = None, @@ -306,7 +307,7 @@ def __init__( self.additional_sources_targets = additional_sources_targets @property - def target_folder(self) -> str: + def target_folder(self) -> Path: return mxmake_files() @property @@ -324,11 +325,11 @@ class MxIni(Template): description: str = "mx configutation file" target_name = "mx.ini" template_name = "mx.ini" - target_folder = "" + target_folder = Path() def __init__( self, - target_folder: str, + target_folder: Path, domains: typing.List[Domain], environment: typing.Union[Environment, None] = None, ) -> None: @@ -357,7 +358,7 @@ class Topics(Template): description: str = "Topics documentation for sphinx" target_name = "" template_name = "topics.md" - target_folder = "" + target_folder = Path() @property def template_variables(self) -> typing.Dict[str, typing.Any]: @@ -386,7 +387,7 @@ class Dependencies(Template): description: str = "Dependencies documentation for sphinx" target_name = "" template_name = "dependencies.md" - target_folder = "" + target_folder = Path() @property def template_variables(self) -> typing.Dict[str, typing.Any]: @@ -426,7 +427,7 @@ class GHActionsTemplate(Template): template_variables = dict() @property - def target_folder(self) -> str: + def target_folder(self) -> Path: return gh_actions_path() diff --git a/src/mxmake/testing/__init__.py b/src/mxmake/testing/__init__.py index 3aaa88e3..c38f581c 100644 --- a/src/mxmake/testing/__init__.py +++ b/src/mxmake/testing/__init__.py @@ -1,5 +1,6 @@ from contextlib import contextmanager from mxmake import templates +from pathlib import Path import doctest import mxdev @@ -11,7 +12,7 @@ def temp_directory(fn): - tempdir = tempfile.mkdtemp() + tempdir = Path(tempfile.mkdtemp()) def wrapper(self): try: @@ -38,9 +39,9 @@ def __init__(self, reset_registry: bool = False): def __call__(self, fn: typing.Callable): def wrapper(*a): - tempdir = tempfile.mkdtemp() - os.environ["MXMAKE_FILES"] = tempdir - os.environ["MXMAKE_GH_ACTIONS_PATH"] = tempdir + tempdir = Path(tempfile.mkdtemp()) + os.environ["MXMAKE_FILES"] = str(tempdir) + os.environ["MXMAKE_GH_ACTIONS_PATH"] = str(tempdir) try: if self.reset_registry: with reset_template_registry(): diff --git a/src/mxmake/tests/test_hook.py b/src/mxmake/tests/test_hook.py index 13238f22..e7cf355f 100644 --- a/src/mxmake/tests/test_hook.py +++ b/src/mxmake/tests/test_hook.py @@ -2,15 +2,13 @@ from mxmake import testing import mxdev -import os -import pathlib import unittest class TestHook(unittest.TestCase): @testing.template_directory() def test_Hook(self, tempdir): - mxini = pathlib.Path(tempdir, "mx.ini") + mxini = tempdir / "mx.ini" with mxini.open("w") as fd: fd.write( "[settings]\n" "mxmake-templates = run-tests run-coverage inexistent" @@ -21,5 +19,6 @@ def test_Hook(self, tempdir): state = mxdev.State(configuration=configuration) hook_.write(state) self.assertEqual( - sorted(os.listdir(tempdir)), ["mx.ini", "run-coverage.sh", "run-tests.sh"] + [entry.name for entry in sorted(tempdir.iterdir())], + ["mx.ini", "run-coverage.sh", "run-tests.sh"], ) diff --git a/src/mxmake/tests/test_parser.py b/src/mxmake/tests/test_parser.py index 2d0b136a..50b7d6dc 100644 --- a/src/mxmake/tests/test_parser.py +++ b/src/mxmake/tests/test_parser.py @@ -3,7 +3,6 @@ from mxmake import testing from mxmake import topics -import os import unittest @@ -37,7 +36,7 @@ def test_MakefileParser(self, tempdir): template.write() - makefile_path = os.path.join(tempdir, "Makefile") + makefile_path = tempdir / "Makefile" makefile_parser = parser.MakefileParser(makefile_path) self.assertEqual( diff --git a/src/mxmake/tests/test_templates.py b/src/mxmake/tests/test_templates.py index 70236a4e..e7bfefe1 100644 --- a/src/mxmake/tests/test_templates.py +++ b/src/mxmake/tests/test_templates.py @@ -5,10 +5,9 @@ from mxmake import testing from mxmake import topics from mxmake import utils +from pathlib import Path import mxdev -import os -import pathlib import typing @@ -44,7 +43,7 @@ class Template(templates.Template): ) @testing.template_directory() - def test_Template(self, tempdir: str): + def test_Template(self, tempdir: Path): # cannot instantiate abstract template with self.assertRaises(TypeError): templates.Template() # type: ignore @@ -63,12 +62,12 @@ class Template(templates.Template): template.write() # write template - with open(os.path.join(tempdir, "target.in"), "w") as f: + with (tempdir / "target.in").open("w") as f: f.write("{{ param }}") environment = Environment(loader=FileSystemLoader(tempdir)) template = Template(environment) template.write() - with open(os.path.join(tempdir, "target.out")) as f: + with (tempdir / "target.out").open() as f: self.assertEqual(f.read(), "value") # check file mode @@ -78,7 +77,7 @@ class Template(templates.Template): # remove remplate removed = template.remove() self.assertTrue(removed) - self.assertFalse(os.path.exists(os.path.join(tempdir, "target.out"))) + self.assertFalse((tempdir / "target.out").exists()) self.assertFalse(template.remove()) @testing.template_directory() @@ -86,7 +85,7 @@ def test_MxIniBoundTemplate(self, tempdir: str): # create test template class Template(templates.MxIniBoundTemplate): name = "template" - target_folder = "" + target_folder = Path() target_name = "" template_name = "" template_variables = {} @@ -120,7 +119,7 @@ class Template(templates.EnvironmentTemplate): @testing.template_directory() def test_TestScript(self, tempdir): - mxini = pathlib.Path(tempdir, "mx.ini") + mxini = tempdir / "mx.ini" with mxini.open("w") as fd: fd.write( "[settings]\n" @@ -158,7 +157,7 @@ def test_TestScript(self, tempdir): ) template.write() - with open(os.path.join(tempdir, "run-tests.sh")) as f: + with (tempdir / "run-tests.sh").open() as f: self.checkOutput( """ #!/usr/bin/env bash @@ -202,7 +201,7 @@ def test_TestScript(self, tempdir): configuration = mxdev.Configuration(mxini, hooks=[hook.Hook()]) template = factory(configuration, templates.get_template_environment()) template.write() - with open(os.path.join(tempdir, "run-tests.sh")) as f: + with (tempdir / "run-tests.sh").open() as f: self.checkOutput( """ #!/usr/bin/env bash @@ -234,7 +233,7 @@ def test_TestScript(self, tempdir): configuration = mxdev.Configuration(mxini, hooks=[hook.Hook()]) template = factory(configuration, templates.get_template_environment()) template.write() - with open(os.path.join(tempdir, "run-tests.sh")) as f: + with (tempdir / "run-tests.sh").open() as f: self.checkOutput( """ #!/usr/bin/env bash @@ -255,7 +254,7 @@ def test_TestScript(self, tempdir): @testing.template_directory() def test_CoverageScript(self, tempdir): - mxini = pathlib.Path(tempdir, "mx.ini") + mxini = tempdir / "mx.ini" with mxini.open("w") as fd: fd.write( "[settings]\n" @@ -321,7 +320,7 @@ def test_CoverageScript(self, tempdir): ) template.write() - with open(os.path.join(tempdir, "run-coverage.sh")) as f: + with (tempdir / "run-coverage.sh").open() as f: self.checkOutput( """ #!/usr/bin/env bash @@ -388,7 +387,7 @@ def test_CoverageScript(self, tempdir): configuration = mxdev.Configuration(mxini, hooks=[hook.Hook()]) template = factory(configuration, templates.get_template_environment()) template.write() - with open(os.path.join(tempdir, "run-coverage.sh")) as f: + with (tempdir / "run-coverage.sh").open() as f: self.checkOutput( """ #!/usr/bin/env bash @@ -433,7 +432,7 @@ def test_CoverageScript(self, tempdir): configuration = mxdev.Configuration(mxini, hooks=[hook.Hook()]) template = factory(configuration, templates.get_template_environment()) template.write() - with open(os.path.join(tempdir, "run-coverage.sh")) as f: + with (tempdir / "run-coverage.sh").open() as f: self.checkOutput( """ #!/usr/bin/env bash @@ -467,7 +466,7 @@ def test_CoverageScript(self, tempdir): @testing.template_directory() def test_PipConf(self, tempdir): - mxini = pathlib.Path(tempdir, "mx.ini") + mxini = tempdir / "mx.ini" with mxini.open("w") as fd: fd.write( "[settings]\n" @@ -492,7 +491,7 @@ def test_PipConf(self, tempdir): ) template.write() - with open(os.path.join(tempdir, "pip.conf")) as f: + with (tempdir / "pip.conf").open() as f: self.checkOutput( """ [global] @@ -509,8 +508,7 @@ def test_AdditionalSourcesTargets(self, tempdir): template = factory(["a", "b"], templates.get_template_environment()) template.write() - path = os.path.join(tempdir, "additional_sources_targets.mk") - with open(path) as f: + with (tempdir / "additional_sources_targets.mk").open() as f: self.checkOutput("ADDITIONAL_SOURCES_TARGETS=$(wildcard a b)", f.read()) @testing.temp_directory @@ -540,7 +538,7 @@ def test_Makefile(self, tempdir): ) template.write() - with open(os.path.join(tempdir, "Makefile")) as f: + with (tempdir / "Makefile").open() as f: self.checkOutput( """ ############################################################################## @@ -798,7 +796,7 @@ def test_MxIni(self, tempdir): template = factory(tempdir, domains, templates.get_template_environment()) template.write() - with open(os.path.join(tempdir, "mx.ini")) as f: + with (tempdir / "mx.ini").open() as f: self.checkOutput( """ [settings] diff --git a/src/mxmake/tests/test_topics.py b/src/mxmake/tests/test_topics.py index 62cba68e..f8e54423 100644 --- a/src/mxmake/tests/test_topics.py +++ b/src/mxmake/tests/test_topics.py @@ -5,7 +5,6 @@ from mxmake import topics import configparser -import os import typing import unittest @@ -89,8 +88,8 @@ def test_get_domain(self): @testing.temp_directory def test_Domain(self, tmpdir): - domain_path = os.path.join(tmpdir, "domain.mk") - with open(domain_path, "w") as f: + domain_path = tmpdir / "domain.mk" + with domain_path.open("w") as f: f.write(MAKEFILE_TEMPLATE) domain = topics.Domain(topic="topic", name="example", file=domain_path) @@ -134,27 +133,27 @@ def test_Domain(self, tmpdir): self.assertEqual(settings[0].description, "Setting A") self.assertEqual(settings[0].default, "A") - out_path = os.path.join(tmpdir, "domain_out.mk") - with open(out_path, "w") as fd: + out_path = tmpdir / "domain_out.mk" + with out_path.open("w") as fd: domain.write_to(fd) - with open(out_path) as fd: + with out_path.open() as fd: out_content = fd.readlines() self.assertEqual(out_content[0], "SETTING_A?=A\n") self.assertEqual(out_content[-1], "\t@rm -f $(EXAMPLE_TARGET)\n") @testing.temp_directory def test_Topic(self, tmpdir): - topicdir = os.path.join(tmpdir, "topic") - os.mkdir(topicdir) - with open(os.path.join(topicdir, "metadata.ini"), "w") as f: + topicdir = tmpdir / "topic" + topicdir.mkdir() + with (topicdir / "metadata.ini").open("w") as f: f.write("[metadata]\n") f.write("title = Title\n") f.write("description = Description\n") - with open(os.path.join(topicdir, "domain-a.mk"), "w") as f: + with (topicdir / "domain-a.mk").open("w") as f: f.write("\n") - with open(os.path.join(topicdir, "domain-b.mk"), "w") as f: + with (topicdir / "domain-b.mk").open("w") as f: f.write("\n") - with open(os.path.join(topicdir, "somethinelse"), "w") as f: + with (topicdir / "somethinelse").open("w") as f: f.write("\n") topic = topics.Topic(name="topic", directory=topicdir) diff --git a/src/mxmake/tests/test_utils.py b/src/mxmake/tests/test_utils.py index 87e7b2e6..557fac2f 100644 --- a/src/mxmake/tests/test_utils.py +++ b/src/mxmake/tests/test_utils.py @@ -1,4 +1,5 @@ from mxmake import utils +from pathlib import Path import os import unittest @@ -9,15 +10,15 @@ def test_namespace(self): self.assertEqual(utils.NAMESPACE, "mxmake-") def test_mxmake_files(self): - self.assertEqual(utils.mxmake_files(), os.path.join(".mxmake", "files")) + self.assertEqual(utils.mxmake_files(), Path(".mxmake") / "files") os.environ["MXMAKE_FILES"] = "other" - self.assertEqual(utils.mxmake_files(), "other") + self.assertEqual(utils.mxmake_files(), Path("other")) del os.environ["MXMAKE_FILES"] def test_gh_actions_path(self): - self.assertEqual(utils.gh_actions_path(), os.path.join(".github", "workflows")) + self.assertEqual(utils.gh_actions_path(), Path(".github") / "workflows") os.environ["MXMAKE_GH_ACTIONS_PATH"] = "other" - self.assertEqual(utils.gh_actions_path(), "other") + self.assertEqual(utils.gh_actions_path(), Path("other")) del os.environ["MXMAKE_GH_ACTIONS_PATH"] def test_ns_name(self): diff --git a/src/mxmake/topics.py b/src/mxmake/topics.py index 1f984a6c..758a4f0e 100644 --- a/src/mxmake/topics.py +++ b/src/mxmake/topics.py @@ -1,12 +1,12 @@ from collections import Counter from dataclasses import dataclass +from pathlib import Path from pkg_resources import iter_entry_points import configparser import functools import io import operator -import os import typing @@ -27,7 +27,7 @@ class Target: class Domain: topic: str name: str - file: str + file: Path def __post_init__(self) -> None: # Runtime dependencies contain the list of dependencies used for @@ -44,9 +44,9 @@ def fqn(self): @property def file_data(self) -> typing.List[str]: - if hasattr(self, "_file_data"): - return self._file_data - with open(self.file) as f: + if (_file_data := getattr(self, "_file_data", None)) is not None: + return _file_data + with self.file.open() as f: self.file_data = f.readlines() return self._file_data @@ -56,8 +56,8 @@ def file_data(self, value: typing.List[str]): @property def config(self) -> configparser.ConfigParser: - if hasattr(self, "_config"): - return self._config + if (_config := getattr(self, "_config", None)) is not None: + return _config data = io.StringIO() for line in self.file_data: if line.startswith("#:"): @@ -136,11 +136,11 @@ def write_to(self, fd: typing.TextIO): @dataclass class Topic: name: str - directory: str + directory: Path def __post_init__(self) -> None: config = configparser.ConfigParser(default_section="metadata") - config.read(os.path.join(self.directory, "metadata.ini")) + config.read(self.directory / "metadata.ini") self.title = config.get("metadata", "title") self.description = config.get("metadata", "description") @@ -149,11 +149,11 @@ def domains(self) -> typing.List[Domain]: return [ Domain( topic=self.name, - name=name[:-3], - file=os.path.join(self.directory, name), + name=name.stem, + file=self.directory / name, ) - for name in sorted(os.listdir(self.directory)) - if name.endswith(".mk") + for name in sorted(self.directory.iterdir()) + if name.suffix == ".mk" ] def domain(self, name: str) -> typing.Optional[Domain]: @@ -293,15 +293,13 @@ def set_domain_runtime_depends(domains: typing.List[Domain]) -> None: # topics shipped within mxmake ############################################################################## -topics_dir = os.path.join(os.path.dirname(__file__), "topics") - -core = Topic(name="core", directory=os.path.join(topics_dir, "core")) -docs = Topic(name="docs", directory=os.path.join(topics_dir, "docs")) -js = Topic(name="js", directory=os.path.join(topics_dir, "js")) -ldap = Topic(name="ldap", directory=os.path.join(topics_dir, "ldap")) -qa = Topic(name="qa", directory=os.path.join(topics_dir, "qa")) -system = Topic(name="system", directory=os.path.join(topics_dir, "system")) -applications = Topic( - name="applications", directory=os.path.join(topics_dir, "applications") -) -i18n = Topic(name="i18n", directory=os.path.join(topics_dir, "i18n")) +topics_dir = Path(__file__).parent / "topics" + +core = Topic(name="core", directory=topics_dir / "core") +docs = Topic(name="docs", directory=topics_dir / "docs") +js = Topic(name="js", directory=topics_dir / "js") +ldap = Topic(name="ldap", directory=topics_dir / "ldap") +qa = Topic(name="qa", directory=topics_dir / "qa") +system = Topic(name="system", directory=topics_dir / "system") +applications = Topic(name="applications", directory=topics_dir / "applications") +i18n = Topic(name="i18n", directory=topics_dir / "i18n") diff --git a/src/mxmake/utils.py b/src/mxmake/utils.py index c10ac342..b6c30e88 100644 --- a/src/mxmake/utils.py +++ b/src/mxmake/utils.py @@ -1,3 +1,5 @@ +from pathlib import Path + import os import typing @@ -5,15 +7,18 @@ NAMESPACE = "mxmake-" -def mxmake_files() -> str: +def mxmake_files() -> Path: """Target folder for mxmake related file generation.""" - return os.environ.get("MXMAKE_FILES", os.path.join(".mxmake", "files")) + return Path(os.environ.get("MXMAKE_FILES", Path(".mxmake") / "files")) -def gh_actions_path() -> str: +def gh_actions_path() -> Path: """Target folder for github actions related file generation.""" - return os.environ.get( - "MXMAKE_GH_ACTIONS_PATH", os.path.join(".github", "workflows") + return Path( + os.environ.get( + "MXMAKE_GH_ACTIONS_PATH", + Path(".github") / "workflows", + ) )