diff --git a/pyproject.toml b/pyproject.toml index 964e9cdb..5359c2d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] description = "One line description of your module" -dependencies = [] # Add project dependencies here, e.g. ["click", "numpy"] +dependencies = ["tomli"] # Add project dependencies here, e.g. ["click", "numpy"] dynamic = ["version"] license.file = "LICENSE" readme = "README.rst" diff --git a/src/python3_pip_skeleton/__main__.py b/src/python3_pip_skeleton/__main__.py index 705ef84f..791b99d0 100644 --- a/src/python3_pip_skeleton/__main__.py +++ b/src/python3_pip_skeleton/__main__.py @@ -7,6 +7,8 @@ from tempfile import TemporaryDirectory from typing import List +import tomli + from . import __version__ __all__ = ["main"] @@ -144,6 +146,26 @@ def verify_not_adopted(root: Path): ) +def obtain_git_author_email(path: Path, force_local=True): + # If we force local then we require there to be a local .git we can look for + # the username and password on. + # If we don't force local then we will try to look for a local .git, if not found + # git will use the global user.[name, email]. + if force_local and not (path / ".git").exists(): + raise FileNotFoundError( + ".git could not be found when searching " + f"for a username and password in {path}" + ) + author = str( + git("--git-dir", path / ".git", "config", "--get", "user.name").strip() + ) + author_email = str( + git("--git-dir", path / ".git", "config", "--get", "user.email").strip() + ) + + return author, author_email + + def new(args): path: Path = args.path @@ -156,23 +178,109 @@ def new(args): else: path.mkdir(parents=True) + if args.full_name and args.email: + author, author_email = args.full_name, args.email + else: + author, author_email = obtain_git_author_email(Path("."), force_local=False) + git("init", "-b", "main", cwd=path) print(f"Created git repo in {path}") merge_skeleton( path=path, org=args.org, - full_name=args.full_name or git("config", "--get", "user.name").strip(), - email=args.email or git("config", "--get", "user.email").strip(), + full_name=author, + email=author_email, from_branch=args.from_branch or "main", package=package, ) cfg_issue = """Missing parameter in setup.cfg. Expected format: -[metadata] -name = example -author = Firstname Lastname -author_email = email@address.com""" + [metadata] + name = example + author = Firstname Lastname + author_email = email@address.com + + ------- pyproject.toml + [[project.authors]] + name = "Firstname Lastname" + email = "email@address.com" +""" + + +def obtain_author_name_email(path: Path) -> tuple: + author: str = "" + author_email: str = "" + file_path_setup_cfg: Path = path / "setup.cfg" + file_path_pyproject_toml: Path = path / "pyproject.toml" + + # Parse for an author name, email. The order of preference used is + # setup.cfg -> pyproject.toml -> .git -> user input. + # Author and Email are recieved together to avoid mismatches from + # obtaining in different places. + + if file_path_setup_cfg.exists(): + try: + conf_cfg = ConfigParser() + conf_cfg.read(file_path_setup_cfg) + + if "metadata" in conf_cfg: + if "author" in conf_cfg["metadata"]: + author = conf_cfg["metadata"]["author"] + if "author_email" in conf_cfg["metadata"]: + author_email = conf_cfg["metadata"]["author_email"] + except Exception as exception: + print( + "\033[1mUnable to parse setup.cfg because of the following error, " + "will try other sources:\033[0m" + ) + print(exception) + print() + + if (not author or not author_email) and file_path_pyproject_toml.exists(): + file = open(file_path_pyproject_toml, "rb") + try: + conf_toml = tomli.load(file) + if "project" in conf_toml and "authors" in conf_toml["project"]: + # pyproject.toml will use "author" or "name" so we look for both + for author_variable_name in ["author", "name"]: + if author_variable_name in conf_toml["project"]["authors"][0]: + author = conf_toml["project"]["authors"][0][ + author_variable_name + ] + if "email" in conf_toml["project"]["authors"][0]: + author_email = conf_toml["project"]["authors"][0]["email"] + except Exception as exception: + # We want to use something else if the pyproject.toml has some errors. + print( + "\033[1mUnable to parse project.toml because of the following error, " + "will try other sources:\033[0m" + ) + print(exception) + print() + file.close() + + if not author or not author_email: + try: + author, author_email = obtain_git_author_email(path) + except FileNotFoundError: + print( + "\033[1mUnable to find a .git in the repo," + "will try other sources\033[0m" + ) + + # If all else fails, just ask the user. + if not author or not author_email: + print(cfg_issue) + print("Enter author name manually:") + author = str(input()) + print("Enter author email manually:") + author_email = str(input()) + + assert author, "Inputted no author" + assert author_email, "Inputted no author_email" + + return author, author_email def existing(args): @@ -181,21 +289,20 @@ def existing(args): assert path.is_dir(), f"Expected {path} to be an existing directory" package = validate_package(args) - file_path: Path = path / "setup.cfg" - assert file_path.is_file(), "Expected a setup.cfg file in the directory." + if not args.force: verify_not_adopted(args.path) - conf = ConfigParser() - conf.read(path / "setup.cfg") - assert "metadata" in conf, cfg_issue - assert "author" in conf["metadata"], cfg_issue - assert "author_email" in conf["metadata"], cfg_issue + if args.full_name and args.email: + author, author_email = args.full_name, args.email + else: + author, author_email = obtain_author_name_email(path) + merge_skeleton( path=args.path, org=args.org, - full_name=conf["metadata"]["author"], - email=conf["metadata"]["author_email"], + full_name=author, + email=author_email, from_branch=args.from_branch or "main", package=package, ) @@ -248,6 +355,12 @@ def main(args=None): sub.add_argument( "--package", default=None, help="Package name, defaults to directory name" ) + sub.add_argument( + "--full-name", default=None, help="Full name, defaults to git config user.name" + ) + sub.add_argument( + "--email", default=None, help="Email address, defaults to git config user.email" + ) sub.add_argument( "--from-branch", default=None, diff --git a/tests/test_adopt.py b/tests/test_adopt.py index dcc8b41f..17aaf273 100644 --- a/tests/test_adopt.py +++ b/tests/test_adopt.py @@ -1,7 +1,8 @@ import subprocess import sys -from os import makedirs +from os import chdir, makedirs from pathlib import Path +from unittest.mock import patch import pytest import toml @@ -23,7 +24,33 @@ def test_cli_version(): assert output.strip() == __version__ -def test_new_module(tmp_path: Path): +@pytest.mark.parametrize( + "extra_args", [(), ("--full-name=Firstname Lastname", "--email=me@myaddress.com")] +) +def test_new_module(extra_args, tmp_path: Path): + if not extra_args: + original_path = Path(".").absolute() + check_output("git", "init", str(tmp_path), cwd=tmp_path) + check_output( + "git", + "--git-dir", + str(tmp_path / ".git"), + "config", + "user.name", + "Firstname Lastname", + cwd=tmp_path, + ) + check_output( + "git", + "--git-dir", + str(tmp_path / ".git"), + "config", + "user.email", + "me@myaddress.com", + cwd=tmp_path, + ) + chdir(tmp_path) + module = tmp_path / "my-module" output = check_output( sys.executable, @@ -32,10 +59,13 @@ def test_new_module(tmp_path: Path): "new", "--org=myorg", "--package=my_module", - "--full-name=Firstname Lastname", - "--email=me@myaddress.com", + *extra_args, str(module), ) + + if not extra_args: + chdir(original_path) + assert output.strip().endswith( "Developer instructions in docs/developer/tutorials/dev-install.rst" ) @@ -72,6 +102,7 @@ def test_new_module(tmp_path: Path): def test_new_module_existing_dir(tmp_path: Path): + print(Path(".").absolute()) module = tmp_path / "my-module" makedirs(module / "existing_dir") @@ -129,8 +160,19 @@ def test_new_module_merge_from_invalid_branch(tmp_path: Path): assert "couldn't find remote ref fail" in str(excinfo.value) -def test_existing_module(tmp_path: Path): +SETUP_CFG = """[metadata] + name = example + author = Firstname Lastname + author_email = email@address.com + """ + + +@pytest.mark.parametrize( + "extra_args", [(), ("--full-name=Firstname Lastname", "--email=me@myaddress.com")] +) +def test_existing_module(extra_args, tmp_path: Path): module = tmp_path / "scanspec" + __main__.git( "clone", "--depth", @@ -140,12 +182,17 @@ def test_existing_module(tmp_path: Path): "https://github.com/dls-controls/scanspec", str(module), ) + + with open(module / "setup.cfg", "w+") as setup_cfg: + setup_cfg.write(SETUP_CFG) + output = check_output( sys.executable, "-m", "python3_pip_skeleton", "existing", "--org=epics-containers", + *extra_args, str(module), ) assert output.endswith( @@ -233,3 +280,61 @@ def test_existing_module_merge_from_invalid_branch(tmp_path: Path): str(module), ) assert "couldn't find remote ref fail" in str(excinfo.value) + + +def test_obtain_git_author_email(tmp_path): + __main__.git("--git-dir", tmp_path / ".git", "init") + __main__.git("--git-dir", tmp_path / ".git", "config", "user.name", "Foo Bar") + __main__.git("--git-dir", tmp_path / ".git", "config", "user.email", "Foo@Bar") + assert __main__.obtain_git_author_email(tmp_path) == ("Foo Bar", "Foo@Bar") + + +def test_obtain_author_name_email_setup_cfg(tmp_path): + cfg_str = """ + [metadata] + author = Foo Bar + author_email = Foo@Bar + """ + with open(tmp_path / "setup.cfg", "w+") as cfg_file: + cfg_file.write(cfg_str) + assert __main__.obtain_author_name_email(tmp_path) == ("Foo Bar", "Foo@Bar") + + +def test_obtain_author_name_email_pyproject_toml(tmp_path): + toml_str = """ + [[project.authors]] + email = "Foo@Bar" + name = "Foo Bar" + """ + with open(tmp_path / "pyproject.toml", "w+") as toml_file: + toml_file.write(toml_str) + assert __main__.obtain_author_name_email(tmp_path) == ("Foo Bar", "Foo@Bar") + + +@patch("python3_pip_skeleton.__main__.input", return_value="Foo") +def test_obtain_author_name_email_botched_cfg_toml(input, tmp_path): + toml_str = """ + email + name = "Foo Bar" + """ + cfg_str = """ + author = Foo Bar + author_email = Foo@Bar + """ + with open(tmp_path / "setup.cfg", "w+") as cfg_file: + cfg_file.write(cfg_str) + with open(tmp_path / "pyproject.toml", "w+") as toml_file: + toml_file.write(toml_str) + assert __main__.obtain_author_name_email(tmp_path) == ("Foo", "Foo") + + +def test_obtain_author_name_email_git(tmp_path): + __main__.git("--git-dir", tmp_path / ".git", "init") + __main__.git("--git-dir", tmp_path / ".git", "config", "user.name", "Foo Bar") + __main__.git("--git-dir", tmp_path / ".git", "config", "user.email", "Foo@Bar") + assert __main__.obtain_author_name_email(tmp_path) == ("Foo Bar", "Foo@Bar") + + +@patch("python3_pip_skeleton.__main__.input", return_value="Foo") +def test_obtain_author_name_email_terminal_output(input, tmp_path): + assert __main__.obtain_author_name_email(tmp_path) == ("Foo", "Foo")