diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bd58d38ba..5f222e0b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,6 +41,9 @@ jobs: - name: Lint with flake8 run: poetry run flake8 + - name: Lint with mypy + run: poetry run mypy . + - name: Print python versions run: | python -V diff --git a/CHANGES b/CHANGES index 63cf5c1dd..ca5340be7 100644 --- a/CHANGES +++ b/CHANGES @@ -70,6 +70,19 @@ $ pip install --user --upgrade --pre libvcs ### Internals +- {issue}`362` [mypy] support added: + + - Basic mypy tests now pass + - Type annotations added, including improved typings for: + + - {meth}`libvcs._internal.subprocess.SubprocessCommand.run` + - {meth}`libvcs._internal.subprocess.SubprocessCommand.Popen` + - {meth}`libvcs._internal.subprocess.SubprocessCommand.check_output` + - {meth}`libvcs._internal.subprocess.run.run` + + - `make mypy` and `make watch_mypy` + - Automatic checking on CI + - {issue}`345` `libvcs.utils` -> `libvcs._internal` to make it more obvious the APIs are strictly closed. - `StrOrPath` -> `StrPath` @@ -86,6 +99,9 @@ $ pip install --user --upgrade --pre libvcs ### Documentation - Document `libvcs.types` +- {issue}`362`: Improve developer documentation to note [mypy] and have tabbed examples for flake8. + +[mypy]: http://mypy-lang.org/ ### Packaging diff --git a/docs/conf.py b/docs/conf.py index 7f77d95c7..247085a36 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ sys.path.insert(0, str(doc_path / "_ext")) # package data -about = {} +about: dict = {} with open(project_root / "libvcs" / "__about__.py") as fp: exec(fp.read(), about) @@ -58,8 +58,8 @@ html_extra_path = ["manifest.json"] html_favicon = "_static/favicon.ico" html_theme = "furo" -html_theme_path = [] -html_theme_options = { +html_theme_path: list = [] +html_theme_options: dict = { "light_logo": "img/libvcs.svg", "dark_logo": "img/libvcs.svg", "footer_icons": [ diff --git a/docs/contributing/workflow.md b/docs/contributing/workflow.md index 0caa2c441..99e64450d 100644 --- a/docs/contributing/workflow.md +++ b/docs/contributing/workflow.md @@ -6,18 +6,24 @@ [poetry] is a required package to develop. +```console +$ git clone https://github.com/vcs-python/libvcs.git ``` -git clone https://github.com/vcs-python/libvcs.git -cd libvcs -poetry install -E "docs test coverage lint format" + +```console +$ cd libvcs +``` + +```console +$ poetry install -E "docs test coverage lint format" ``` Makefile commands prefixed with `watch_` will watch files and rerun. ## Tests -``` -poetry run py.test +```console +$ poetry run py.test ``` Helpers: `make test` Rerun tests on file change: `make watch_test` (requires [entr(1)]) @@ -43,13 +49,105 @@ Rebuild docs on file change: `make watch_docs` (requires [entr(1)]) Rebuild docs and run server via one terminal: `make dev_docs` (requires above, and a `make(1)` with `-J` support, e.g. GNU Make) -## Formatting / Linting +## Formatting + +The project uses [black] and [isort] (one after the other). Configurations are in `pyproject.toml` +and `setup.cfg`: + +- `make black isort`: Run `black` first, then `isort` to handle import nuances + +## Linting + +[flake8] and [mypy] run via CI in our GitHub Actions. See the configuration in `pyproject.toml` and +`setup.cfg`. + +### flake8 + +[flake8] provides fast, reliable, barebones styling and linting. + +````{tab} Command + +poetry: + +```console +$ poetry run flake8 +``` + +If you setup manually: + +```console +$ flake8 +``` + +```` + +````{tab} make + +```console +$ make flake8 +``` + +```` + +````{tab} Watch + +```console +$ make watch_flake8 +``` -The project uses [black] and [isort] (one after the other) and runs [flake8] via CI. See the -configuration in `pyproject.toml` and `setup.cfg`: +requires [`entr(1)`]. + +```` + +````{tab} Configuration + +See `[flake8]` in setup.cfg. + +```{literalinclude} ../../setup.cfg +:language: ini +:start-at: "[flake8]" +:end-before: "[isort]" + +``` + +```` + +### mypy + +[mypy] is used for static type checking. + +````{tab} Command + +poetry: + +```console +$ poetry run mypy . +``` + +If you setup manually: + +```console +$ mypy . +``` + +```` + +````{tab} make + +```console +$ make mypy +``` + +```` + +````{tab} Watch + +```console +$ make watch_mypy +``` -`make black isort`: Run `black` first, then `isort` to handle import nuances `make flake8`, to watch -(requires `entr(1)`): `make watch_flake8` +requires [`entr(1)`]. +```` ## Releasing @@ -67,6 +165,8 @@ Update `__version__` in `__about__.py` and `pyproject.toml`:: [poetry]: https://python-poetry.org/ [entr(1)]: http://eradman.com/entrproject/ +[`entr(1)`]: http://eradman.com/entrproject/ [black]: https://github.com/psf/black [isort]: https://pypi.org/project/isort/ [flake8]: https://flake8.pycqa.org/ +[mypy]: http://mypy-lang.org/ diff --git a/libvcs/_internal/run.py b/libvcs/_internal/run.py index b83280ba8..be0248d7c 100644 --- a/libvcs/_internal/run.py +++ b/libvcs/_internal/run.py @@ -178,7 +178,7 @@ def run( shell: bool = False, cwd: Optional[StrOrBytesPath] = None, env: Optional[_ENV] = None, - universal_newlines: Optional[bool] = None, + universal_newlines: bool = False, startupinfo: Optional[Any] = None, creationflags: int = 0, restore_signals: bool = True, @@ -262,7 +262,7 @@ def progress_cb(output, timestamp): umask=umask, ) - all_output = [] + all_output: list[str] = [] code = None line = None while code is None: @@ -270,7 +270,7 @@ def progress_cb(output, timestamp): # output = console_to_str(proc.stdout.readline()) # all_output.append(output) - if callback and callable(callback): + if callback and callable(callback) and proc.stderr is not None: line = console_to_str(proc.stderr.read(128)) if line: callback(output=line, timestamp=datetime.datetime.now()) diff --git a/libvcs/_internal/subprocess.py b/libvcs/_internal/subprocess.py index 1c3ab5321..9f8a8f544 100644 --- a/libvcs/_internal/subprocess.py +++ b/libvcs/_internal/subprocess.py @@ -204,7 +204,78 @@ class SubprocessCommand(SkipDefaultFieldsReprMixin): encoding: Optional[str] = None errors: Optional[str] = None - def Popen(self, **kwargs) -> subprocess.Popen: + # user, group, extra_groups, umask were added in 3.9 + @overload + def Popen( + self, + args: Optional[_CMD] = ..., + universal_newlines: bool = ..., + *, + text: Optional[bool] = ..., + encoding: str, + errors: Optional[str] = ..., + ) -> subprocess.Popen[str]: + ... + + @overload + def Popen( + self, + args: Optional[_CMD] = ..., + universal_newlines: bool = ..., + *, + text: Optional[bool] = ..., + encoding: Optional[str] = ..., + errors: str, + ) -> subprocess.Popen[str]: + ... + + @overload + def Popen( + self, + args: Optional[_CMD] = ..., + *, + universal_newlines: Literal[True], + # where the *real* keyword only args start + text: Optional[bool] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + ) -> subprocess.Popen[str]: + ... + + @overload + def Popen( + self, + args: Optional[_CMD] = ..., + universal_newlines: bool = ..., + *, + text: Literal[True], + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + ) -> subprocess.Popen[str]: + ... + + @overload + def Popen( + self, + args: Optional[_CMD] = ..., + universal_newlines: Literal[False] = ..., + *, + text: Literal[None, False] = ..., + encoding: None = ..., + errors: None = ..., + ) -> subprocess.Popen[bytes]: + ... + + def Popen( + self, + args: Optional[_CMD] = None, + universal_newlines: Optional[bool] = None, + *, + text: Optional[bool] = None, + encoding: Optional[str] = None, + errors: Optional[str] = None, + **kwargs, + ) -> subprocess.Popen[Any]: """Run commands :class:`subprocess.Popen`, optionally overrides via kwargs. Parameters @@ -218,7 +289,17 @@ def Popen(self, **kwargs) -> subprocess.Popen: >>> proc = cmd.Popen(stdout=subprocess.PIPE) >>> proc.communicate() # doctest: +SKIP """ - return subprocess.Popen(**dataclasses.replace(self, **kwargs).__dict__) + return subprocess.Popen( + **dataclasses.replace( + self, + args=args or self.args, + encoding=encoding, + errors=errors, + text=text, + universal_newlines=universal_newlines, + **kwargs, + ).__dict__, + ) def check_call(self, **kwargs) -> int: """Run command :func:`subprocess.check_call`, optionally overrides via kwargs. @@ -237,15 +318,79 @@ def check_call(self, **kwargs) -> int: return subprocess.check_call(**dataclasses.replace(self, **kwargs).__dict__) @overload - def check_output(self, input: Optional[str] = None, **kwargs) -> str: + def check_output( + self, + universal_newlines: bool = ..., + *, + input: Optional[Union[str, bytes]] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + text: Literal[True], + **kwargs, + ) -> str: ... @overload - def check_output(self, input: Optional[bytes] = None, **kwargs) -> bytes: + def check_output( + self, + universal_newlines: Optional[bool] = ..., + *, + input: Optional[Union[str, bytes]] = ..., + encoding: str, + errors: Optional[str] = ..., + text: Optional[bool] = ..., + **kwargs, + ) -> str: ... + @overload + def check_output( + self, + universal_newlines: bool = ..., + *, + input: Optional[Union[str, bytes]] = ..., + encoding: Optional[str] = ..., + errors: str, + text: Optional[bool] = ..., + **kwargs, + ) -> str: + ... + + @overload + def check_output( + self, + universal_newlines: Literal[True] = ..., + *, + input: Optional[Union[str, bytes]] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + text: Optional[bool] = ..., + **kwargs, + ) -> str: + ... + + @overload def check_output( - self, input: Optional[Union[str, bytes]] = None, **kwargs + self, + universal_newlines: Literal[False], + *, + input: Optional[Union[str, bytes]] = ..., + encoding: None = ..., + errors: None = ..., + text: Literal[None, False] = ..., + **kwargs, + ) -> bytes: + ... + + def check_output( + self, + universal_newlines: Optional[bool] = None, + *, + input: Optional[Union[str, bytes]] = None, + encoding: Optional[str] = None, + errors: Optional[str] = None, + text: Optional[bool] = None, + **kwargs, ) -> Union[bytes, str]: r"""Run command :func:`subprocess.check_output`, optionally overrides via kwargs. @@ -280,14 +425,90 @@ def check_output( params.pop("stdout") return subprocess.check_output(input=input, **params) + @overload def run( self, - input: Optional[Union[str, bytes]] = None, - timeout: Optional[int] = None, - check: bool = False, + universal_newlines: bool = ..., + *, + capture_output: bool = ..., + check: bool = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + input: Optional[str] = ..., + text: Literal[True], + ) -> subprocess.CompletedProcess[str]: + ... + + @overload + def run( + self, + universal_newlines: bool = ..., + *, + capture_output: bool = ..., + check: bool = ..., + encoding: str, + errors: Optional[str] = ..., + input: Optional[str] = ..., + text: Optional[bool] = ..., + ) -> subprocess.CompletedProcess[str]: + ... + + @overload + def run( + self, + universal_newlines: bool = ..., + *, + capture_output: bool = ..., + check: bool = ..., + encoding: Optional[str] = ..., + errors: str, + input: Optional[str] = ..., + text: Optional[bool] = ..., + ) -> subprocess.CompletedProcess[str]: + ... + + @overload + def run( + self, + *, + universal_newlines: Literal[True], + # where the *real* keyword only args start + capture_output: bool = ..., + check: bool = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + input: Optional[str] = ..., + text: Optional[bool] = ..., + ) -> subprocess.CompletedProcess[str]: + ... + + @overload + def run( + self, + universal_newlines: Literal[False] = ..., + *, + capture_output: bool = ..., + check: bool = ..., + encoding: None = ..., + errors: None = ..., + input: Optional[bytes] = ..., + text: Literal[None, False] = ..., + ) -> subprocess.CompletedProcess[bytes]: + ... + + def run( + self, + universal_newlines: Optional[bool] = None, + *, capture_output: bool = False, + check: bool = False, + encoding: Optional[str] = None, + errors: Optional[str] = None, + input: Optional[Union[str, bytes]] = None, + text: Optional[bool] = None, + timeout: Optional[float] = None, **kwargs, - ) -> subprocess.CompletedProcess: + ) -> subprocess.CompletedProcess[Any]: r"""Run command in :func:`subprocess.run`, optionally overrides via kwargs. Parameters @@ -344,9 +565,15 @@ def run( b'' """ return subprocess.run( - **dataclasses.replace(self, **kwargs).__dict__, - input=input, + **dataclasses.replace( + self, + universal_newlines=universal_newlines, + errors=errors, + text=text, + **kwargs, + ).__dict__, + check=check, capture_output=capture_output, + input=input, timeout=timeout, - check=check, ) diff --git a/libvcs/cmd/git.py b/libvcs/cmd/git.py index 75daa9531..5711cd8d2 100644 --- a/libvcs/cmd/git.py +++ b/libvcs/cmd/git.py @@ -250,7 +250,7 @@ def clone( if template is not None: local_flags.append(f"--template={template}") if separate_git_dir is not None: - local_flags.append(f"--separate-git-dir={separate_git_dir}") + local_flags.append(f"--separate-git-dir={separate_git_dir!r}") if (filter := kwargs.pop("filter", None)) is not None: local_flags.append(f"--filter={filter}") if depth is not None: @@ -388,7 +388,7 @@ def fetch( local_flags: list[str] = [] if submodule_prefix is not None: - local_flags.append(f"--submodule-prefix={submodule_prefix}") + local_flags.append(f"--submodule-prefix={submodule_prefix!r}") if (filter := kwargs.pop("filter", None)) is not None: local_flags.append(f"--filter={filter}") if depth is not None: @@ -561,7 +561,7 @@ def rebase( if onto: local_flags.extend(["--onto", onto]) if context: - local_flags.extend(["--C", context]) + local_flags.extend(["--C", str(context)]) if exec: local_flags.extend(["--exec", shlex.quote(exec)]) @@ -864,7 +864,7 @@ def pull( # Fetch-related arguments # if submodule_prefix is not None: - local_flags.append(f"--submodule-prefix={submodule_prefix}") + local_flags.append(f"--submodule-prefix={submodule_prefix!r}") if (filter := kwargs.pop("filter", None)) is not None: local_flags.append(f"--filter={filter}") if depth is not None: @@ -1007,7 +1007,7 @@ def init( if template is not None: local_flags.append(f"--template={template}") if separate_git_dir is not None: - local_flags.append(f"--separate-git-dir={separate_git_dir}") + local_flags.append(f"--separate-git-dir={separate_git_dir!r}") if object_format is not None: local_flags.append(f"--object-format={object_format}") if branch is not None: @@ -1164,7 +1164,7 @@ def reset( if refresh is True: local_flags.append("--refresh") if pathspec_from_file is not None: - local_flags.append(f"--pathspec_from_file={pathspec_from_file}") + local_flags.append(f"--pathspec_from_file={pathspec_from_file!r}") # HEAD to commit form if soft is True: diff --git a/libvcs/cmd/svn.py b/libvcs/cmd/svn.py index c870495db..a848b4231 100644 --- a/libvcs/cmd/svn.py +++ b/libvcs/cmd/svn.py @@ -257,7 +257,7 @@ def auth( def blame( self, - target: pathlib.Path, + target: StrOrBytesPath, *, revision: Union[RevisionLiteral, str] = None, verbose: Optional[bool] = None, diff --git a/libvcs/conftest.py b/libvcs/conftest.py index 368cd60f3..a62e89446 100644 --- a/libvcs/conftest.py +++ b/libvcs/conftest.py @@ -11,7 +11,7 @@ from faker import Faker from libvcs._internal.run import run, which -from libvcs.projects.git import GitProject, GitRemoteDict +from libvcs.projects.git import GitProject, GitRemote skip_if_git_missing = pytest.mark.skipif( not which("git"), reason="git is not available" @@ -322,11 +322,10 @@ def git_repo(projects_path: pathlib.Path, git_remote_repo: pathlib.Path): url=f"file://{git_remote_repo}", dir=str(projects_path / "git_repo"), remotes={ - "origin": GitRemoteDict( - **{ - "push_url": f"file://{git_remote_repo}", - "fetch_url": f"file://{git_remote_repo}", - } + "origin": GitRemote( + name="origin", + push_url=f"file://{git_remote_repo}", + fetch_url=f"file://{git_remote_repo}", ) }, ) diff --git a/libvcs/projects/base.py b/libvcs/projects/base.py index 2712b76e3..bab3778af 100644 --- a/libvcs/projects/base.py +++ b/libvcs/projects/base.py @@ -1,7 +1,7 @@ """Base class for VCS Project plugins.""" import logging import pathlib -from typing import NamedTuple +from typing import NamedTuple, Optional, Tuple from urllib import parse as urlparse from libvcs._internal.run import CmdLoggingAdapter, run @@ -12,7 +12,7 @@ class VCSLocation(NamedTuple): url: str - rev: str + rev: Optional[str] def convert_pip_url(pip_url: str) -> VCSLocation: @@ -35,11 +35,14 @@ def convert_pip_url(pip_url: str) -> VCSLocation: class BaseProject: """Base class for repositories.""" - #: log command output to buffer log_in_real_time = None + """Log command output to buffer""" - #: vcs app name, e.g. 'git' bin_name = "" + """VCS app name, e.g. 'git'""" + + schemes: Tuple[str, ...] = () + """List of supported schemes to register in ``urlparse.uses_netloc``""" def __init__(self, *, url: str, dir: StrPath, progress_callback=None, **kwargs): r""" diff --git a/libvcs/projects/git.py b/libvcs/projects/git.py index 6eb877b06..da0d22ea5 100644 --- a/libvcs/projects/git.py +++ b/libvcs/projects/git.py @@ -53,7 +53,7 @@ def to_tuple(self): GitProjectRemoteDict = Dict[str, GitRemote] GitFullRemoteDict = Dict[str, GitRemoteDict] -GitRemotesArgs = Union[None, GitFullRemoteDict, Dict[str, str]] +GitRemotesArgs = Union[None, GitFullRemoteDict, GitProjectRemoteDict, Dict[str, str]] @dataclasses.dataclass @@ -123,6 +123,9 @@ def from_stdout(cls, value: str): re.VERBOSE | re.MULTILINE, ) matches = pattern.search(value) + + if matches is None: + raise Exception("Could not find match") return cls(**matches.groupdict()) @@ -154,6 +157,7 @@ def convert_pip_url(pip_url: str) -> VCSLocation: class GitProject(BaseProject): bin_name = "git" schemes = ("git", "git+http", "git+https", "git+ssh", "git+git", "git+file") + _remotes: GitProjectRemoteDict def __init__( self, *, url: str, dir: StrPath, remotes: GitRemotesArgs = None, **kwargs @@ -226,7 +230,11 @@ def __init__( ) elif isinstance(remote_url, dict): self._remotes[remote_name] = GitRemote( - **{**remote_url, "name": remote_name} + **{ + "fetch_url": remote_url["fetch_url"], + "push_url": remote_url["push_url"], + "name": remote_name, + } ) elif isinstance(remote_url, GitRemote): self._remotes[remote_name] = remote_url @@ -238,13 +246,15 @@ def __init__( push_url=url, ) super().__init__(url=url, dir=dir, **kwargs) - self.url = self.chomp_protocol( - ( - self._remotes.get("origin") - if "origin" in self._remotes - else next(iter(self._remotes.items()))[1] - ).fetch_url + + origin = ( + self._remotes.get("origin") + if "origin" in self._remotes + else next(iter(self._remotes.items()))[1] ) + if origin is None: + raise Exception("Missing origin") + self.url = self.chomp_protocol(origin.fetch_url) @classmethod def from_pip_url(cls, pip_url, **kwargs): @@ -376,6 +386,8 @@ def update_repo(self, set_remotes: bool = False, *args, **kwargs): show_ref_output, re.MULTILINE, ) + if m is None: + raise exc.CommandError("Could not fetch remote names") git_remote_name = m.group("git_remote_name") git_tag = m.group("git_tag") self.log.debug("git_remote_name: %s" % git_remote_name) @@ -497,15 +509,15 @@ def remotes(self, flat=False) -> Dict: remotes = {} cmd = self.run(["remote"]) - ret = filter(None, cmd.split("\n")) + ret: filter[str] = filter(None, cmd.split("\n")) for remote_name in ret: - remotes[remote_name] = ( - self.remote(remote_name) if flat else self.remote(remote_name).to_dict() - ) + remote = self.remote(remote_name) + if remote is not None: + remotes[remote_name] = remote if flat else remote.to_dict() return remotes - def remote(self, name, **kwargs) -> GitRemote: + def remote(self, name, **kwargs) -> Optional[GitRemote]: """Get the fetch and push URL for a specified remote name. Parameters diff --git a/tests/_internal/subprocess/test_SubprocessCommand.py b/tests/_internal/subprocess/test_SubprocessCommand.py index 38ecff03d..73361b0d6 100644 --- a/tests/_internal/subprocess/test_SubprocessCommand.py +++ b/tests/_internal/subprocess/test_SubprocessCommand.py @@ -132,7 +132,8 @@ def test_init_and_check_output(args: list, kwargs: dict, expected_result: Any): ids=idfn, ) def test_run(tmp_path: pathlib.Path, args: list, kwargs: dict, run_kwargs: dict): - cmd = SubprocessCommand(*args, cwd=tmp_path, **kwargs) + kwargs["cwd"] = tmp_path + cmd = SubprocessCommand(*args, **kwargs) response = cmd.run(**run_kwargs) assert response.returncode == 0 diff --git a/tests/projects/test_base.py b/tests/projects/test_base.py index 6deff672a..9d5ca3f41 100644 --- a/tests/projects/test_base.py +++ b/tests/projects/test_base.py @@ -43,7 +43,7 @@ def test_convert_pip_url(): def test_progress_callback( - capsys: pytest.LogCaptureFixture, + capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path, git_remote_repo: pathlib.Path, ): diff --git a/tests/projects/test_git.py b/tests/projects/test_git.py index d9bcaf692..ed8b95eb7 100644 --- a/tests/projects/test_git.py +++ b/tests/projects/test_git.py @@ -311,11 +311,15 @@ def test_remotes( expected = lazy_remote_expected(**locals()) assert len(expected.keys()) > 0 for expected_remote_name, expected_remote_url in expected.items(): - assert ( - expected_remote_name, - expected_remote_url, - expected_remote_url, - ) == git_repo.remote(expected_remote_name).to_tuple() + remote = git_repo.remote(expected_remote_name) + assert remote is not None + + if remote is not None: + assert ( + expected_remote_name, + expected_remote_url, + expected_remote_url, + ) == remote.to_tuple() @pytest.mark.parametrize( @@ -424,7 +428,10 @@ def test_remotes_update_repo( git_repo: GitProject = constructor(**lazy_constructor_options(**locals())) git_repo.obtain() - git_repo._remotes |= lazy_remote_dict(**locals()) + git_repo._remotes |= { + k: GitRemote(*v) if isinstance(v, dict) else v + for k, v in lazy_remote_dict(**locals()).items() + } git_repo.update_repo(set_remotes=True) expected = lazy_remote_expected(**locals()) @@ -558,7 +565,10 @@ def test_set_remote(git_repo: GitProject, repo_name: str, new_repo_url: str): assert isinstance( git_repo.remote(name=repo_name), GitRemote ), "remote() returns GitRemote" - assert "file:///" in git_repo.remote(name=repo_name).fetch_url, "new value set" + remote = git_repo.remote(name=repo_name) + assert remote is not None, "Remote should exist" + if remote is not None: + assert "file:///" in remote.fetch_url, "new value set" assert "myrepo" in git_repo.remotes(), ".remotes() returns new remote" @@ -570,9 +580,12 @@ def test_set_remote(git_repo: GitProject, repo_name: str, new_repo_url: str): mynewremote = git_repo.set_remote(name="myrepo", url=new_repo_url, overwrite=True) - assert ( - new_repo_url in git_repo.remote(name="myrepo").fetch_url - ), "Running remove_set should overwrite previous remote" + remote = git_repo.remote(name="myrepo") + assert remote is not None + if remote is not None: + assert ( + new_repo_url in remote.fetch_url + ), "Running remove_set should overwrite previous remote" def test_get_git_version(git_repo: GitProject): diff --git a/tests/projects/test_svn.py b/tests/projects/test_svn.py index 0ccc39b0b..3f67e3846 100644 --- a/tests/projects/test_svn.py +++ b/tests/projects/test_svn.py @@ -7,7 +7,6 @@ from libvcs._internal.run import which from libvcs.conftest import CreateProjectCallbackFixtureProtocol from libvcs.projects.svn import SubversionProject -from libvcs.shortcuts import create_project_from_pip_url if not which("svn"): pytestmark = pytest.mark.skip(reason="svn is not available") @@ -16,11 +15,9 @@ def test_repo_svn(tmp_path: pathlib.Path, svn_remote_repo): repo_name = "my_svn_project" - svn_repo = create_project_from_pip_url( - **{ - "pip_url": f"svn+file://{svn_remote_repo}", - "dir": tmp_path / repo_name, - } + svn_repo = SubversionProject( + url=f"file://{svn_remote_repo}", + dir=str(tmp_path / repo_name), ) svn_repo.obtain()