From 6e3c3f0ea5fa70d3cd7cb47b725f9afa06ba0ea5 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Sun, 15 Mar 2026 21:12:24 +0100 Subject: [PATCH 01/12] Rename format_blocks function --- magicli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/magicli.py b/magicli.py index c335879..1b1bd9f 100644 --- a/magicli.py +++ b/magicli.py @@ -198,7 +198,7 @@ def help_from_function(function, name=None): message = [name] if name else [] message.append(function.__name__) message.extend(map(format_kwarg, inspect.signature(function).parameters.values())) - return format_message([["usage:", " ".join(message)]]) + return format_blocks([["usage:", " ".join(message)]]) def format_kwarg(kwarg): @@ -221,12 +221,12 @@ def help_from_module(module): if commands := get_commands(module): message.append(["commands:", *commands]) - return format_message(message) + return format_blocks(message) -def format_message(blocks): +def format_blocks(blocks, sep="\n "): """Formats blocks of text with proper indentation.""" - return "\n\n".join("\n ".join(block) for block in blocks) + return "\n\n".join(sep.join(block) for block in blocks) def load_module(name): From 2a29ccdbf200624feaae6a69b6ac04ca5409fc74 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Sun, 15 Mar 2026 21:12:48 +0100 Subject: [PATCH 02/12] Add single file layout --- magicli.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/magicli.py b/magicli.py index 1b1bd9f..2abc5d2 100644 --- a/magicli.py +++ b/magicli.py @@ -267,10 +267,15 @@ def get_project_name(): """ Detect project name from project structure. """ - flat_layout = [path.stem for path in Path().glob("*.py")] - src_layout = [path.parent.name for path in Path().glob("*/__init__.py")] + single_file_layout = [path.stem for path in Path().glob("*.py")] + flat_layout = [ + path.parent.name + for path in Path().glob("*/__init__.py") + if path.parent.name != "tests" + ] + src_layout = [path.parent.name for path in Path().glob("src/*/__init__.py")] - if len(names := flat_layout + src_layout) == 1: + if len(names := single_file_layout + flat_layout + src_layout) == 1: return names[0] if name := input("CLI name: "): From 97bb27d79a3c66f1ae2262e328c03705386eb0c4 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Sun, 15 Mar 2026 21:13:18 +0100 Subject: [PATCH 03/12] Add CLI docstring --- magicli.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/magicli.py b/magicli.py index 2abc5d2..40ee1fd 100644 --- a/magicli.py +++ b/magicli.py @@ -284,10 +284,50 @@ def get_project_name(): raise SystemExit(1) -def cli(): +def get_output(command): + try: + output = subprocess.run(command.split(), capture_output=True, text=True).stdout + except FileNotFoundError: + return None + return output[:-1] if output else None + + +def get_homepage(url=None): + url = url or get_output("git remote get-url origin") or "" + if url.endswith(".git"): + url = url[:-4] + if url.startswith("git@"): + url = "https://" + url.replace(":", "/")[4:] + return url + + +def cli( + name="", + author="", + email="", + readme="", + license="", + description="", + homepage="", +): """ + magiCLI✨ + Generates a "pyproject.toml" configuration file for a module and sets up the project script. The CLI name must be the same as the module name. + + usage: + cli [option] + + options: + --name + --author + --email + --readme + --license + --description + --homepage + -v, --version """ pyproject = Path("pyproject.toml") if ( From dd5b1e137fef239e1a423dbbc83abc0ae1d65b7c Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Sun, 15 Mar 2026 21:14:43 +0100 Subject: [PATCH 04/12] Add more information to pyproject.toml --- magicli.py | 61 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/magicli.py b/magicli.py index 40ee1fd..30325db 100644 --- a/magicli.py +++ b/magicli.py @@ -6,6 +6,7 @@ import importlib import inspect +import subprocess import sys from importlib import metadata from pathlib import Path @@ -22,7 +23,7 @@ def magicli(): argv = sys.argv[1:] if name == "magicli": - raise SystemExit(call(cli, argv)) + raise SystemExit(call(cli, argv, sys.modules["magicli"])) module = load_module(name) name = name.replace("-", "_") @@ -336,23 +337,55 @@ def cli( ): raise SystemExit(1) - name = get_project_name() - pyproject.write_text( - f"""\ -[build-system] -requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] -build-backend = "setuptools.build_meta" + name = name or get_project_name() + project = [ + "[project]", + f'name = "{name}"', + 'dynamic = ["version"]', + 'dependencies = ["magicli<3"]', + ] -[project] -name = "{name}" -dynamic = ["version"] -dependencies = ["magicli<3"] + if not author: + author = get_output("git config --get user.name") + if not email: + email = get_output("git config --get user.email") -[project.scripts] -{name} = "magicli:magicli" -""" + authors = [f'{k}="{v}"' for k, v in {"name": author, "email": email}.items() if v] + if authors: + project.append(f"authors = [{{{', '.join(authors)}}}]") + + if readme or Path(readme := "README.md").exists(): + project.append(f'readme = "{readme}"') + + if license or Path(license := "LICENSE").exists(): + project.append(f'license-files = ["{license}"]') + + if not description: + try: + if doc := (importlib.import_module(name).__doc__ or "").split("\n\n"): + description = " ".join( + [l for line in doc[0].splitlines() if (l := line.strip())] + ) + except ModuleNotFoundError: + pass + + if description: + project.append(f'description = "{description}"') + + blocks = [project, ["[project.scripts]", f'{name} = "magicli:magicli"']] + if homepage or (homepage := get_homepage()): + blocks.append(["[project.urls]", f'Home = "{homepage}"']) + + blocks.append( + [ + "[build-system]", + 'requires = ["setuptools>=80", "setuptools-scm[simple]>=8"]', + 'build-backend = "setuptools.build_meta"', + ] ) + pyproject.write_text(format_blocks(blocks, sep="\n") + "\n") + message = ["pyproject.toml created! ✨"] if Path(".git").exists(): message.append("You can specify the version with `git tag`") From c15ae8d72197763ba00a949ccc6c2712930e2526 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 08:38:14 +0100 Subject: [PATCH 05/12] Refactor cli() --- magicli.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/magicli.py b/magicli.py index 30325db..ca44062 100644 --- a/magicli.py +++ b/magicli.py @@ -302,6 +302,14 @@ def get_homepage(url=None): return url +def get_description(name): + try: + if doc := (importlib.import_module(name).__doc__ or "").split("\n\n"): + return " ".join([l for line in doc[0].splitlines() if (l := line.strip())]) + except ModuleNotFoundError: + pass + + def cli( name="", author="", @@ -338,6 +346,10 @@ def cli( raise SystemExit(1) name = name or get_project_name() + author = author or get_output("git config --get user.name") + email = email or get_output("git config --get user.email") + authors = [f'{k}="{v}"' for k, v in {"name": author, "email": email}.items() if v] + project = [ "[project]", f'name = "{name}"', @@ -345,12 +357,6 @@ def cli( 'dependencies = ["magicli<3"]', ] - if not author: - author = get_output("git config --get user.name") - if not email: - email = get_output("git config --get user.email") - - authors = [f'{k}="{v}"' for k, v in {"name": author, "email": email}.items() if v] if authors: project.append(f"authors = [{{{', '.join(authors)}}}]") @@ -360,19 +366,11 @@ def cli( if license or Path(license := "LICENSE").exists(): project.append(f'license-files = ["{license}"]') - if not description: - try: - if doc := (importlib.import_module(name).__doc__ or "").split("\n\n"): - description = " ".join( - [l for line in doc[0].splitlines() if (l := line.strip())] - ) - except ModuleNotFoundError: - pass - - if description: + if description or (description := get_description(name)): project.append(f'description = "{description}"') blocks = [project, ["[project.scripts]", f'{name} = "magicli:magicli"']] + if homepage or (homepage := get_homepage()): blocks.append(["[project.urls]", f'Home = "{homepage}"']) From 364b671f56441b909d32faca9480f8f5a3cb8a7d Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 08:38:45 +0100 Subject: [PATCH 06/12] Update tests --- tests/fixtures.py | 70 +++++++++++----------- tests/test_cli.py | 96 +++++++++++++++++++++++++------ tests/test_magicli.py | 5 +- tests/test_parse_short_options.py | 1 - 4 files changed, 117 insertions(+), 55 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index a12ff87..538fa47 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,54 +1,58 @@ import os -import shutil from pathlib import Path +from tempfile import TemporaryDirectory import pytest -@pytest.fixture() -def setup(): +def _setup(filenames, dirname=None): cwd = Path.cwd() - path = Path("tests", "tmp") - - # Make sure the directory does not exist - if path.exists(): - shutil.rmtree(path) - - path.mkdir(exist_ok=True) - Path(path, "module.py").touch() - os.chdir(path) - - yield path - + directory = TemporaryDirectory() + if dirname: + Path(directory.name, dirname).mkdir() + os.chdir(directory.name) + else: + os.chdir(directory.name) + for filename in filenames: + Path(directory.name, filename).touch() + return directory, cwd + + +def _teardown(directory, cwd): + directory.cleanup() os.chdir(cwd) - shutil.rmtree(path) @pytest.fixture() -def two_py(): - file = Path("two.py") - file.touch() - - yield file - - file.unlink() +def with_tempdir(): + directory, cwd = _setup(["module.py"]) + yield directory.name + _teardown(directory, cwd) @pytest.fixture() -def pyproject_toml(): - file = Path("pyproject.toml") - file.touch() +def with_two_files(): + directory, cwd = _setup(["module.py", "two.py"]) + yield + _teardown(directory, cwd) - yield file - file.unlink() +@pytest.fixture() +def pyproject(): + directory, cwd = _setup(["pyproject.toml", "module.py"]) + yield Path(directory.name, "pyproject.toml") + _teardown(directory, cwd) @pytest.fixture() -def dotgit(): - dir = Path(".git") - dir.mkdir(exist_ok=True) +def with_git(): + directory, cwd = _setup([], dirname=".git") + yield + _teardown(directory, cwd) - yield dir - shutil.rmtree(dir) +@pytest.fixture() +def empty_directory(): + directory, cwd = _setup([]) + yield + _teardown(directory, cwd) diff --git a/tests/test_cli.py b/tests/test_cli.py index 467e5a4..d52352c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,13 +2,19 @@ from unittest import mock import pytest -from fixtures import pyproject_toml, setup, two_py, dotgit +from fixtures import empty_directory, pyproject, with_git, with_tempdir, with_two_files -from magicli import cli, get_project_name +from magicli import cli, get_description, get_homepage, get_output, get_project_name + + +def module(name): + module = type(pytest)(name) + module.__doc__ = "docstring" + return module @mock.patch("builtins.input", lambda _: "two") -def test_correct_name_input(setup, two_py): +def test_correct_name_input(with_two_files): cli() path = Path("pyproject.toml") @@ -17,46 +23,98 @@ def test_correct_name_input(setup, two_py): assert 'name = "two"' in f.read() -@mock.patch("builtins.input", lambda *args: "n") -def test_automatic_name(setup): +@mock.patch("builtins.input", lambda *_: "y") +def test_automatic_name(pyproject): cli() - with Path("pyproject.toml").open() as f: - assert 'name = "module"' in f.read() + + assert 'name = "module"' in pyproject.read_text() @mock.patch("builtins.input", lambda *args: "y") -def test_overwrite_pyproject_toml(setup, pyproject_toml): +def test_overwrite_pyproject_toml(pyproject): cli() - with pyproject_toml.open() as f: - assert 'name = "module"' in f.read() + + assert 'name = "module"' in pyproject.read_text() @mock.patch("builtins.input", lambda *args: "") -def test_empty_cli_name_failure(setup, two_py): +def test_empty_cli_name_failure(with_two_files): with pytest.raises(SystemExit) as error: get_project_name() assert error.value.code == 1 -def test_on_git_repo(capsys, setup): - cli() +def test_with_git_repo(capsys, with_git): + cli(name="_") out, _ = capsys.readouterr() - with_git = ( + without_git = ( "Error: Not a git repo. Run `git init`. Specify version with `git tag`.\n" ) - without_git = "You can specify the version with `git tag`\n" + with_git = "You can specify the version with `git tag`\n" assert out.endswith(with_git) assert without_git not in out -def test_git_repo(capsys, setup, dotgit): - cli() +def test_without_git_repo(capsys, empty_directory): + cli(name="_") out, _ = capsys.readouterr() - with_git = ( + without_git = ( "Error: Not a git repo. Run `git init`. Specify version with `git tag`.\n" ) - without_git = "You can specify the version with `git tag`\n" + with_git = "You can specify the version with `git tag`\n" assert out.endswith(without_git) assert with_git not in out + + +def test_get_output(): + assert get_output("ls") != None + assert get_output("-") == None + + +def test_get_homepage(): + for url in [ + "https://github.com/PatrickElmer/magicli.git", + "git@github.com:PatrickElmer/magicli.git", + ]: + assert get_homepage(url) == "https://github.com/PatrickElmer/magicli" + + +def test_get_description(): + assert get_description("magicli") != None + + +def test_cli_with_kwargs(with_tempdir): + cli( + name="name", + author="Patrick Elmer", + email="patrick@elmer.ws", + readme="README.md", + license="LICENSE", + description="docstring", + homepage="https://github.com/PatrickElmer/magicli", + ) + assert ( + Path("pyproject.toml").read_text() + == """\ +[project] +name = "name" +dynamic = ["version"] +dependencies = ["magicli<3"] +authors = [{name="Patrick Elmer", email="patrick@elmer.ws"}] +readme = "README.md" +license-files = ["LICENSE"] +description = "docstring" + +[project.scripts] +name = "magicli:magicli" + +[project.urls] +Home = "https://github.com/PatrickElmer/magicli" + +[build-system] +requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] +build-backend = "setuptools.build_meta" +""" + ) diff --git a/tests/test_magicli.py b/tests/test_magicli.py index 6281e9f..77df0a3 100644 --- a/tests/test_magicli.py +++ b/tests/test_magicli.py @@ -1,8 +1,9 @@ import sys -from unittest import mock from functools import partial +from unittest import mock import pytest +from fixtures import pyproject from magicli import magicli @@ -85,7 +86,7 @@ def test_module_not_found(): @mock.patch("builtins.input", lambda *args: "n") -def test_module_is_magicli(): +def test_module_is_magicli(pyproject): sys.argv = ["magicli"] with pytest.raises(SystemExit) as error: magicli() diff --git a/tests/test_parse_short_options.py b/tests/test_parse_short_options.py index d9b7004..4d6b7b2 100644 --- a/tests/test_parse_short_options.py +++ b/tests/test_parse_short_options.py @@ -1,4 +1,3 @@ -from functools import partial from inspect import Parameter, _ParameterKind import pytest From c7ce09533f196f841f6f8890da191d3c37c06cd4 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 16:49:05 +0100 Subject: [PATCH 07/12] Update gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fa3165f..86072c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ _* -.pytest_cache +.*_cache .vscode .venv build From f9e30690bd685f0103a8e77a8abb4a10edbfc3fb Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 16:49:26 +0100 Subject: [PATCH 08/12] Lint code --- magicli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/magicli.py b/magicli.py index ca44062..d5838a3 100644 --- a/magicli.py +++ b/magicli.py @@ -295,8 +295,7 @@ def get_output(command): def get_homepage(url=None): url = url or get_output("git remote get-url origin") or "" - if url.endswith(".git"): - url = url[:-4] + url = url.removesuffix(".git") if url.startswith("git@"): url = "https://" + url.replace(":", "/")[4:] return url @@ -305,7 +304,9 @@ def get_homepage(url=None): def get_description(name): try: if doc := (importlib.import_module(name).__doc__ or "").split("\n\n"): - return " ".join([l for line in doc[0].splitlines() if (l := line.strip())]) + return " ".join( + [stripped for line in doc[0].splitlines() if (stripped := line.strip())] + ) except ModuleNotFoundError: pass From 92c59dde9ed8c5c21d056f3e7580b06c63be158d Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 16:50:15 +0100 Subject: [PATCH 09/12] Lint tests --- tests/fixtures.py | 10 +++++----- tests/test_cli.py | 10 +++++----- tests/test_magicli.py | 2 +- tests/test_parse_kwarg.py | 6 +++--- tests/test_parse_short_options.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 538fa47..44ef5f6 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -23,35 +23,35 @@ def _teardown(directory, cwd): os.chdir(cwd) -@pytest.fixture() +@pytest.fixture def with_tempdir(): directory, cwd = _setup(["module.py"]) yield directory.name _teardown(directory, cwd) -@pytest.fixture() +@pytest.fixture def with_two_files(): directory, cwd = _setup(["module.py", "two.py"]) yield _teardown(directory, cwd) -@pytest.fixture() +@pytest.fixture def pyproject(): directory, cwd = _setup(["pyproject.toml", "module.py"]) yield Path(directory.name, "pyproject.toml") _teardown(directory, cwd) -@pytest.fixture() +@pytest.fixture def with_git(): directory, cwd = _setup([], dirname=".git") yield _teardown(directory, cwd) -@pytest.fixture() +@pytest.fixture def empty_directory(): directory, cwd = _setup([]) yield diff --git a/tests/test_cli.py b/tests/test_cli.py index d52352c..3284bfa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -30,14 +30,14 @@ def test_automatic_name(pyproject): assert 'name = "module"' in pyproject.read_text() -@mock.patch("builtins.input", lambda *args: "y") +@mock.patch("builtins.input", lambda *_: "y") def test_overwrite_pyproject_toml(pyproject): cli() assert 'name = "module"' in pyproject.read_text() -@mock.patch("builtins.input", lambda *args: "") +@mock.patch("builtins.input", lambda *_: "") def test_empty_cli_name_failure(with_two_files): with pytest.raises(SystemExit) as error: get_project_name() @@ -69,8 +69,8 @@ def test_without_git_repo(capsys, empty_directory): def test_get_output(): - assert get_output("ls") != None - assert get_output("-") == None + assert get_output("ls") is not None + assert get_output("-") is None def test_get_homepage(): @@ -82,7 +82,7 @@ def test_get_homepage(): def test_get_description(): - assert get_description("magicli") != None + assert get_description("magicli") is not None def test_cli_with_kwargs(with_tempdir): diff --git a/tests/test_magicli.py b/tests/test_magicli.py index 77df0a3..68dcb48 100644 --- a/tests/test_magicli.py +++ b/tests/test_magicli.py @@ -85,7 +85,7 @@ def test_module_not_found(): magicli() -@mock.patch("builtins.input", lambda *args: "n") +@mock.patch("builtins.input", return_value="n") def test_module_is_magicli(pyproject): sys.argv = ["magicli"] with pytest.raises(SystemExit) as error: diff --git a/tests/test_parse_kwarg.py b/tests/test_parse_kwarg.py index f879d26..6cb0acf 100644 --- a/tests/test_parse_kwarg.py +++ b/tests/test_parse_kwarg.py @@ -29,9 +29,9 @@ def test_parse_kwarg_bool_and_none(default, result): def test_get_type(): - assert get_type(Parameter("a", PK, annotation=int)) == int - assert get_type(Parameter("b", PK, default=1)) == int - assert get_type(Parameter("c", PK)) == str + assert get_type(Parameter("a", PK, annotation=int)) is int + assert get_type(Parameter("b", PK, default=1)) is int + assert get_type(Parameter("c", PK)) is str def test_args_and_kwargs(): diff --git a/tests/test_parse_short_options.py b/tests/test_parse_short_options.py index 4d6b7b2..a90705e 100644 --- a/tests/test_parse_short_options.py +++ b/tests/test_parse_short_options.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize( - ["default", "result"], + ("default", "result"), [ (None, True), (True, False), From 8ff2baf9e50a397e84d7fbe4c612caff594b4852 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 16:57:49 +0100 Subject: [PATCH 10/12] Remove readme and license from cli args --- magicli.py | 6 ++---- tests/fixtures.py | 7 +++++++ tests/test_cli.py | 13 +++++++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/magicli.py b/magicli.py index d5838a3..8850f07 100644 --- a/magicli.py +++ b/magicli.py @@ -315,8 +315,6 @@ def cli( name="", author="", email="", - readme="", - license="", description="", homepage="", ): @@ -361,10 +359,10 @@ def cli( if authors: project.append(f"authors = [{{{', '.join(authors)}}}]") - if readme or Path(readme := "README.md").exists(): + if Path(readme := "README.md").exists(): project.append(f'readme = "{readme}"') - if license or Path(license := "LICENSE").exists(): + if Path(license := "LICENSE").exists(): project.append(f'license-files = ["{license}"]') if description or (description := get_description(name)): diff --git a/tests/fixtures.py b/tests/fixtures.py index 44ef5f6..b526fe3 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -30,6 +30,13 @@ def with_tempdir(): _teardown(directory, cwd) +@pytest.fixture +def with_readme_and_license(): + directory, cwd = _setup(["README.md", "LICENSE"]) + yield directory.name + _teardown(directory, cwd) + + @pytest.fixture def with_two_files(): directory, cwd = _setup(["module.py", "two.py"]) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3284bfa..6e82c3d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,14 @@ from unittest import mock import pytest -from fixtures import empty_directory, pyproject, with_git, with_tempdir, with_two_files +from fixtures import ( + empty_directory, + pyproject, + with_git, + with_readme_and_license, + with_tempdir, + with_two_files, +) from magicli import cli, get_description, get_homepage, get_output, get_project_name @@ -85,13 +92,11 @@ def test_get_description(): assert get_description("magicli") is not None -def test_cli_with_kwargs(with_tempdir): +def test_cli_with_kwargs(with_readme_and_license): cli( name="name", author="Patrick Elmer", email="patrick@elmer.ws", - readme="README.md", - license="LICENSE", description="docstring", homepage="https://github.com/PatrickElmer/magicli", ) From b10e1b501837204cc2639fb8fd2a57cb0c423c34 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 21:03:17 +0100 Subject: [PATCH 11/12] Lint code --- magicli.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/magicli.py b/magicli.py index 8850f07..f3e5049 100644 --- a/magicli.py +++ b/magicli.py @@ -286,14 +286,18 @@ def get_project_name(): def get_output(command): + """Return the stdout of a shell command or None on failure.""" try: - output = subprocess.run(command.split(), capture_output=True, text=True).stdout + output = subprocess.run( + command.split(), capture_output=True, text=True, check=False + ).stdout except FileNotFoundError: return None - return output[:-1] if output else None + return output.removesuffix("\n") if output else None def get_homepage(url=None): + """Return a homepage url from a git remote url.""" url = url or get_output("git remote get-url origin") or "" url = url.removesuffix(".git") if url.startswith("git@"): @@ -302,6 +306,7 @@ def get_homepage(url=None): def get_description(name): + """Return the first paragraph of a module's docstring if available.""" try: if doc := (importlib.import_module(name).__doc__ or "").split("\n\n"): return " ".join( @@ -309,6 +314,7 @@ def get_description(name): ) except ModuleNotFoundError: pass + return None def cli( @@ -362,8 +368,8 @@ def cli( if Path(readme := "README.md").exists(): project.append(f'readme = "{readme}"') - if Path(license := "LICENSE").exists(): - project.append(f'license-files = ["{license}"]') + if Path(license_file := "LICENSE").exists(): + project.append(f'license-files = ["{license_file}"]') if description or (description := get_description(name)): project.append(f'description = "{description}"') @@ -381,7 +387,7 @@ def cli( ] ) - pyproject.write_text(format_blocks(blocks, sep="\n") + "\n") + pyproject.write_text(format_blocks(blocks, sep="\n") + "\n", encoding="utf-8") message = ["pyproject.toml created! ✨"] if Path(".git").exists(): From 81dcd0571a0c8b640f76b81aaaf7e6489fb15295 Mon Sep 17 00:00:00 2001 From: Patrick Elmer Date: Wed, 18 Mar 2026 21:07:48 +0100 Subject: [PATCH 12/12] Update docstring --- magicli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/magicli.py b/magicli.py index f3e5049..85402e3 100644 --- a/magicli.py +++ b/magicli.py @@ -331,14 +331,12 @@ def cli( The CLI name must be the same as the module name. usage: - cli [option] + magicli [option] options: --name --author --email - --readme - --license --description --homepage -v, --version