diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f284a7a..3c0e126 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -27,10 +27,6 @@ jobs: - windows-latest - macos-latest python-config: - - version: "3.8" - tox-env: "py38" - - version: "3.9" - tox-env: "py39" - version: "3.10" tox-env: "py310" - version: "3.11" diff --git a/CHANGES.md b/CHANGES.md index 35d5c77..1145e79 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,16 @@ ## Changes +## 5.0.0 (unreleased) + +**Breaking Changes:** +- Drop support for Python 3.8 and 3.9. Minimum required version is now Python 3.10. + [jensens] + +**Code Modernization:** +- Modernize type hints to use Python 3.10+ syntax (PEP 604: `X | Y` instead of `Union[X, Y]`) +- Use built-in generic types (`list`, `dict`, `tuple`) instead of `typing.List`, `typing.Dict`, `typing.Tuple` + [jensens] + ## 4.1.2 (unreleased) - Fix #54: Add `fixed` install mode for non-editable installations to support production and Docker deployments. The new `editable` mode replaces `direct` as the default (same behavior, clearer naming). The `direct` mode is now deprecated but still works with a warning. Install modes: `editable` (with `-e`, for development), `fixed` (without `-e`, for production/Docker), `skip` (clone only). diff --git a/CLAUDE.md b/CLAUDE.md index fc7b0b5..ab7577a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,7 +77,7 @@ make coverage-html # Run tests + combine + open HTML report Coverage is automatically collected and combined from all matrix test runs in GitHub Actions: **Process:** -1. Each test job (Python 3.8-3.14, Ubuntu/Windows/macOS) uploads its `.coverage.*` file as an artifact +1. Each test job (Python 3.10-3.14, Ubuntu/Windows/macOS) uploads its `.coverage.*` file as an artifact 2. A dedicated `coverage` job downloads all artifacts 3. Coverage is combined using `coverage combine` 4. Reports are generated: @@ -129,7 +129,7 @@ isort src/mxdev ### Testing Multiple Python Versions (using uvx tox with uv) ```bash -# Run tests on all supported Python versions (Python 3.8-3.14) +# Run tests on all supported Python versions (Python 3.10-3.14) # This uses uvx to run tox with tox-uv plugin for much faster testing (10-100x speedup) uvx --with tox-uv tox @@ -254,7 +254,7 @@ The codebase follows a three-phase pipeline: 1. **Minimal dependencies**: Only `packaging` at runtime - no requests, no YAML parsers 2. **Standard library first**: Uses `configparser`, `urllib`, `threading` instead of third-party libs 3. **No pip invocation**: mxdev generates files; users run pip separately -4. **Backward compatibility**: Supports Python 3.8+ with version detection for Git commands +4. **Backward compatibility**: Supports Python 3.10+ with version detection for Git commands ## Configuration System @@ -403,7 +403,7 @@ myext-package_setting = value - **Formatting**: Black-compatible (max line length: 120) - **Import sorting**: isort with `force_alphabetical_sort = true`, `force_single_line = true` -- **Type hints**: Use throughout (Python 3.8+ compatible) +- **Type hints**: Use throughout (Python 3.10+ compatible) - **Path handling**: Prefer `pathlib.Path` over `os.path` for path operations - Use `pathlib.Path().as_posix()` for cross-platform path comparison - Use `/` operator for path joining: `Path("dir") / "file.txt"` @@ -424,9 +424,9 @@ The project uses GitHub Actions for continuous integration, configured in [.gith **Test Job:** - **Matrix testing** across: - - Python versions: 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 + - Python versions: 3.10, 3.11, 3.12, 3.13, 3.14 - Operating systems: Ubuntu, Windows, macOS - - Total: 21 combinations (7 Python × 3 OS) + - Total: 15 combinations (5 Python × 3 OS) - Uses: `uvx --with tox-uv tox -e py{version}` - Leverages `astral-sh/setup-uv@v7` action for uv installation @@ -674,7 +674,7 @@ gh pr checks ## Requirements -- **Python**: 3.8+ +- **Python**: 3.10+ - **pip**: 23+ (required for proper operation) - **Runtime dependencies**: Only `packaging` - **VCS tools**: Install git, svn, hg, bzr, darcs as needed for VCS operations diff --git a/Makefile b/Makefile index 61607a1..80523b1 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ PRIMARY_PYTHON?=3.14 # Minimum required Python version. # Default: 3.9 -PYTHON_MIN_VERSION?=3.7 +PYTHON_MIN_VERSION?=3.10 # 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 d7d0b6a..0c27119 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ keywords = ["pip", "vcs", "git", "development"] authors = [ {name = "MX Stack Developers", email = "dev@bluedynamics.com" } ] -requires-python = ">=3.8" +requires-python = ">=3.10" license = { text = "BSD 2-Clause License" } classifiers = [ "Development Status :: 5 - Production/Stable", @@ -15,8 +15,6 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -173,7 +171,7 @@ directory = "htmlcov" [tool.tox] requires = ["tox>=4", "tox-uv>=1"] -env_list = ["lint", "py38", "py39", "py310", "py311", "py312", "py313", "py314"] +env_list = ["lint", "py310", "py311", "py312", "py313", "py314"] [tool.tox.env_run_base] description = "Run tests with pytest and coverage" diff --git a/src/mxdev/config.py b/src/mxdev/config.py index ffab3f3..3fdeeaf 100644 --- a/src/mxdev/config.py +++ b/src/mxdev/config.py @@ -17,17 +17,17 @@ def to_bool(value): class Configuration: - settings: typing.Dict[str, str] - overrides: typing.Dict[str, str] - ignore_keys: typing.List[str] - packages: typing.Dict[str, typing.Dict[str, str]] - hooks: typing.Dict[str, typing.Dict[str, str]] + settings: dict[str, str] + overrides: dict[str, str] + ignore_keys: list[str] + packages: dict[str, dict[str, str]] + hooks: dict[str, dict[str, str]] def __init__( self, mxini: str, - override_args: typing.Dict = {}, - hooks: typing.List["Hook"] = [], + override_args: dict = {}, + hooks: list["Hook"] = [], ) -> None: logger.debug("Read configuration") data = read_with_included(mxini) @@ -164,9 +164,9 @@ def out_constraints(self) -> str: return self.settings.get("constraints-out", "constraints-mxdev.txt") @property - def package_keys(self) -> typing.List[str]: + def package_keys(self) -> list[str]: return [k.lower() for k in self.packages] @property - def override_keys(self) -> typing.List[str]: + def override_keys(self) -> list[str]: return [k.lower() for k in self.overrides] diff --git a/src/mxdev/hooks.py b/src/mxdev/hooks.py index cfad663..ff55330 100644 --- a/src/mxdev/hooks.py +++ b/src/mxdev/hooks.py @@ -30,11 +30,11 @@ def load_hooks() -> list: return [ep.load()() for ep in load_eps_by_group("mxdev") if ep.name == "hook"] -def read_hooks(state: State, hooks: typing.List[Hook]) -> None: +def read_hooks(state: State, hooks: list[Hook]) -> None: for hook in hooks: hook.read(state) -def write_hooks(state: State, hooks: typing.List[Hook]) -> None: +def write_hooks(state: State, hooks: list[Hook]) -> None: for hook in hooks: hook.write(state) diff --git a/src/mxdev/including.py b/src/mxdev/including.py index 3f22658..3fb16a3 100644 --- a/src/mxdev/including.py +++ b/src/mxdev/including.py @@ -10,10 +10,10 @@ def resolve_dependencies( - file_or_url: typing.Union[str, Path], + file_or_url: str | Path, tmpdir: str, http_parent=None, -) -> typing.List[Path]: +) -> list[Path]: """Resolve dependencies of a file or url The result is a list of Path objects, starting with the @@ -69,7 +69,7 @@ def resolve_dependencies( return file_list -def read_with_included(file_or_url: typing.Union[str, Path]) -> ConfigParser: +def read_with_included(file_or_url: str | Path) -> ConfigParser: """Read a file or url and include all referenced files, Parse the result as a ConfigParser and return it. diff --git a/src/mxdev/processing.py b/src/mxdev/processing.py index 2f66892..f212a4d 100644 --- a/src/mxdev/processing.py +++ b/src/mxdev/processing.py @@ -13,11 +13,11 @@ def process_line( line: str, - package_keys: typing.List[str], - override_keys: typing.List[str], - ignore_keys: typing.List[str], + package_keys: list[str], + override_keys: list[str], + ignore_keys: list[str], variety: str, -) -> typing.Tuple[typing.List[str], typing.List[str]]: +) -> tuple[list[str], list[str]]: """Take line from a constraints or requirements file and process it recursively. The line is taken as is unless one of the following cases matches: @@ -69,11 +69,11 @@ def process_line( def process_io( fio: typing.IO, - requirements: typing.List[str], - constraints: typing.List[str], - package_keys: typing.List[str], - override_keys: typing.List[str], - ignore_keys: typing.List[str], + requirements: list[str], + constraints: list[str], + package_keys: list[str], + override_keys: list[str], + ignore_keys: list[str], variety: str, ) -> None: """Read lines from an open file and trigger processing of each line @@ -91,17 +91,17 @@ def process_io( def resolve_dependencies( file_or_url: str, - package_keys: typing.List[str], - override_keys: typing.List[str], - ignore_keys: typing.List[str], + package_keys: list[str], + override_keys: list[str], + ignore_keys: list[str], variety: str = "r", -) -> typing.Tuple[typing.List[str], typing.List[str]]: +) -> tuple[list[str], list[str]]: """Takes a file or url, loads it and trigger to recursivly processes its content. returns tuple of requirements and constraints """ - requirements: typing.List[str] = [] - constraints: typing.List[str] = [] + requirements: list[str] = [] + constraints: list[str] = [] if not file_or_url.strip(): logger.info("mxdev is configured to run without input requirements!") return ([], []) @@ -210,7 +210,7 @@ def fetch(state: State) -> None: ) -def write_dev_sources(fio, packages: typing.Dict[str, typing.Dict[str, typing.Any]]): +def write_dev_sources(fio, packages: dict[str, dict[str, typing.Any]]): """Create requirements configuration for fetched source packages.""" if not packages: return @@ -231,9 +231,7 @@ def write_dev_sources(fio, packages: typing.Dict[str, typing.Dict[str, typing.An fio.write("\n\n") -def write_dev_overrides( - fio, overrides: typing.Dict[str, str], package_keys: typing.List[str] -): +def write_dev_overrides(fio, overrides: dict[str, str], package_keys: list[str]): """Create requirements configuration for overridden packages.""" fio.write("#" * 79 + "\n") fio.write("# mxdev constraint overrides\n") @@ -247,7 +245,7 @@ def write_dev_overrides( fio.write("\n\n") -def write_main_package(fio, settings: typing.Dict[str, str]): +def write_main_package(fio, settings: dict[str, str]): """Write main package if configured.""" main_package = settings.get("main-package") if main_package: diff --git a/src/mxdev/state.py b/src/mxdev/state.py index 872df10..9526e52 100644 --- a/src/mxdev/state.py +++ b/src/mxdev/state.py @@ -8,5 +8,5 @@ @dataclass class State: configuration: Configuration - requirements: typing.List[str] = field(default_factory=list) - constraints: typing.List[str] = field(default_factory=list) + requirements: list[str] = field(default_factory=list) + constraints: list[str] = field(default_factory=list) diff --git a/src/mxdev/vcs/bazaar.py b/src/mxdev/vcs/bazaar.py index 42d0021..7d1796f 100644 --- a/src/mxdev/vcs/bazaar.py +++ b/src/mxdev/vcs/bazaar.py @@ -21,9 +21,9 @@ def bzr_branch(self, **kwargs): path = self.source["path"] url = self.source["url"] if os.path.exists(path): - self.output((logger.info, "Skipped branching existing package %r." % name)) + self.output((logger.info, f"Skipped branching existing package {name!r}.")) return - self.output((logger.info, "Branched %r with bazaar." % name)) + self.output((logger.info, f"Branched {name!r} with bazaar.")) env = dict(os.environ) env.pop("PYTHONPATH", None) cmd = subprocess.Popen( @@ -42,7 +42,7 @@ def bzr_pull(self, **kwargs): name = self.source["name"] path = self.source["path"] url = self.source["url"] - self.output((logger.info, "Updated %r with bazaar." % name)) + self.output((logger.info, f"Updated {name!r} with bazaar.")) env = dict(os.environ) env.pop("PYTHONPATH", None) cmd = subprocess.Popen( @@ -67,12 +67,12 @@ def checkout(self, **kwargs): self.update(**kwargs) elif self.matches(): self.output( - (logger.info, "Skipped checkout of existing package %r." % name) + (logger.info, f"Skipped checkout of existing package {name!r}.") ) else: raise BazaarError( - "Source URL for existing package %r differs. " - "Expected %r." % (name, self.source["url"]) + "Source URL for existing package {!r} differs. " + "Expected {!r}.".format(name, self.source["url"]) ) else: return self.bzr_branch(**kwargs) @@ -115,8 +115,8 @@ def update(self, **kwargs): name = self.source["name"] if not self.matches(): raise BazaarError( - "Can't update package %r because its URL doesn't match." % name + f"Can't update package {name!r} because its URL doesn't match." ) if self.status() != "clean" and not kwargs.get("force", False): - raise BazaarError("Can't update package %r because it's dirty." % name) + raise BazaarError(f"Can't update package {name!r} because it's dirty.") return self.bzr_pull(**kwargs) diff --git a/src/mxdev/vcs/common.py b/src/mxdev/vcs/common.py index 619ec68..a04849b 100644 --- a/src/mxdev/vcs/common.py +++ b/src/mxdev/vcs/common.py @@ -22,7 +22,7 @@ def print_stderr(s: str): # taken from # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python -def which(name_root: str, default: typing.Union[str, None] = None) -> str: +def which(name_root: str, default: str | None = None) -> str: if platform.system() == "Windows": # http://www.voidspace.org.uk/python/articles/command_line.shtml#pathext pathext = os.environ["PATHEXT"] @@ -44,7 +44,7 @@ def which(name_root: str, default: typing.Union[str, None] = None) -> str: sys.exit(1) -def version_sorted(inp: typing.List, *args, **kwargs) -> typing.List: +def version_sorted(inp: list, *args, **kwargs) -> list: """ Sorts components versions, it means that numeric parts of version treats as numeric and string as string. @@ -74,8 +74,8 @@ class WCError(Exception): class BaseWorkingCopy(abc.ABC): - def __init__(self, source: typing.Dict[str, typing.Any]): - self._output: typing.List[typing.Tuple[typing.Any, str]] = [] + def __init__(self, source: dict[str, typing.Any]): + self._output: list[tuple[typing.Any, str]] = [] self.output = self._output.append self.source = source @@ -90,42 +90,40 @@ def should_update(self, **kwargs) -> bool: elif update.lower() in ("false", "no"): update = False else: - raise ValueError("Unknown value for 'update': %s" % update) + raise ValueError(f"Unknown value for 'update': {update}") return update @abc.abstractmethod - def checkout(self, **kwargs) -> typing.Union[str, None]: ... + def checkout(self, **kwargs) -> str | None: ... @abc.abstractmethod - def status(self, **kwargs) -> typing.Union[typing.Tuple[str, str], str]: ... + def status(self, **kwargs) -> tuple[str, str] | str: ... @abc.abstractmethod def matches(self) -> bool: ... @abc.abstractmethod - def update(self, **kwargs) -> typing.Union[str, None]: ... + def update(self, **kwargs) -> str | None: ... -def yesno( - question: str, default: bool = True, all: bool = True -) -> typing.Union[str, bool]: +def yesno(question: str, default: bool = True, all: bool = True) -> str | bool: if default: - question = "%s [Yes/no" % question - answers: typing.Dict[typing.Union[str, bool], typing.Tuple] = { + question = f"{question} [Yes/no" + answers: dict[str | bool, tuple] = { False: ("n", "no"), True: ("", "y", "yes"), } else: - question = "%s [yes/No" % question + question = f"{question} [yes/No" answers = { False: ("", "n", "no"), True: ("y", "yes"), } if all: answers["all"] = ("a", "all") - question = "%s/all] " % question + question = f"{question}/all] " else: - question = "%s] " % question + question = f"{question}] " while 1: answer = input(question).lower() for option in answers: @@ -140,14 +138,14 @@ def yesno( # XXX: one lock, one name input_lock = output_lock = threading.RLock() -_workingcopytypes: typing.Dict[str, typing.Type[BaseWorkingCopy]] = {} +_workingcopytypes: dict[str, type[BaseWorkingCopy]] = {} -def get_workingcopytypes() -> typing.Dict[str, typing.Type[BaseWorkingCopy]]: +def get_workingcopytypes() -> dict[str, type[BaseWorkingCopy]]: if _workingcopytypes: return _workingcopytypes group = "mxdev.workingcopytypes" - addons: dict[str, typing.Type[BaseWorkingCopy]] = {} + addons: dict[str, type[BaseWorkingCopy]] = {} for entrypoint in load_eps_by_group(group): key = entrypoint.name workingcopytype = entrypoint.load() @@ -165,7 +163,7 @@ def get_workingcopytypes() -> typing.Dict[str, typing.Type[BaseWorkingCopy]]: class WorkingCopies: def __init__( self, - sources: typing.Dict[str, typing.Dict], + sources: dict[str, dict], threads=5, smart_threading=True, ): @@ -176,8 +174,8 @@ def __init__( self.workingcopytypes = get_workingcopytypes() def _separate_https_packages( - self, packages: typing.List[str] - ) -> typing.Tuple[typing.List[str], typing.List[str]]: + self, packages: list[str] + ) -> tuple[list[str], list[str]]: """Separate HTTPS packages from others for smart threading. Returns (https_packages, other_packages) @@ -251,7 +249,7 @@ def checkout(self, packages: typing.Iterable[str], **kwargs) -> None: # Normal processing (smart_threading disabled or threads=1) self._checkout_impl(packages_list, **kwargs) - def _checkout_impl(self, packages: typing.List[str], **kwargs) -> None: + def _checkout_impl(self, packages: list[str], **kwargs) -> None: """Internal implementation of checkout logic.""" the_queue: queue.Queue = queue.Queue() if "update" in kwargs and not isinstance(kwargs["update"], bool): @@ -263,21 +261,24 @@ def _checkout_impl(self, packages: typing.List[str], **kwargs) -> None: kwargs["update"] = False else: logger.error( - "Unknown value '%s' for always-checkout option." % kwargs["update"] + "Unknown value '{}' for always-checkout option.".format( + kwargs["update"] + ) ) sys.exit(1) kwargs.setdefault("submodules", "always") # XXX: submodules is git related, move to GitWorkingCopy if kwargs["submodules"] not in ["always", "never", "checkout", "recursive"]: logger.error( - "Unknown value '%s' for update-git-submodules option." - % kwargs["submodules"] + "Unknown value '{}' for update-git-submodules option.".format( + kwargs["submodules"] + ) ) sys.exit(1) for name in packages: kw = kwargs.copy() if name not in self.sources: - logger.error("Checkout failed. No source defined for '%s'." % name) + logger.error(f"Checkout failed. No source defined for '{name}'.") sys.exit(1) source = self.sources[name] vcs = source["vcs"] @@ -302,16 +303,16 @@ def _checkout_impl(self, packages: typing.List[str], **kwargs) -> None: if answer == "all": kwargs["force"] = True else: - logger.info("Skipped update of '%s'." % name) + logger.info(f"Skipped update of '{name}'.") continue logger.info("Queued '%s' for checkout.", name) the_queue.put_nowait((wc, wc.checkout, kw)) self.process(the_queue) - def matches(self, source: typing.Dict[str, str]) -> bool: + def matches(self, source: dict[str, str]) -> bool: name = source["name"] if name not in self.sources: - logger.error("Checkout failed. No source defined for '%s'." % name) + logger.error(f"Checkout failed. No source defined for '{name}'.") sys.exit(1) source = self.sources[name] try: @@ -329,12 +330,10 @@ def matches(self, source: typing.Dict[str, str]) -> bool: logger.exception("Can not get matches!") sys.exit(1) - def status( - self, source: typing.Dict[str, str], **kwargs - ) -> typing.Union[str, typing.Tuple[str, str]]: + def status(self, source: dict[str, str], **kwargs) -> str | tuple[str, str]: name = source["name"] if name not in self.sources: - logger.error("Status failed. No source defined for '%s'." % name) + logger.error(f"Status failed. No source defined for '{name}'.") sys.exit(1) source = self.sources[name] try: @@ -345,7 +344,7 @@ def status( sys.exit(1) wc = wc_class(source) if wc is None: - logger.error("Unknown repository type '%s'." % vcs) + logger.error(f"Unknown repository type '{vcs}'.") sys.exit(1) return wc.status(**kwargs) except WCError: @@ -394,7 +393,7 @@ def update(self, packages: typing.Iterable[str], **kwargs) -> None: # Normal processing (smart_threading disabled or threads=1) self._update_impl(packages_list, **kwargs) - def _update_impl(self, packages: typing.List[str], **kwargs) -> None: + def _update_impl(self, packages: list[str], **kwargs) -> None: """Internal implementation of update logic.""" the_queue: queue.Queue = queue.Queue() for name in packages: @@ -409,7 +408,7 @@ def _update_impl(self, packages: typing.List[str], **kwargs) -> None: sys.exit(1) wc = wc_class(source) if wc.status() != "clean" and not kw.get("force", False): - print_stderr("The package '%s' is dirty." % name) + print_stderr(f"The package '{name}' is dirty.") answer = yesno( "Do you want to update it anyway?", default=False, all=True ) @@ -418,7 +417,7 @@ def _update_impl(self, packages: typing.List[str], **kwargs) -> None: if answer == "all": kwargs["force"] = True else: - logger.info("Skipped update of '%s'." % name) + logger.info(f"Skipped update of '{name}'.") continue logger.info("Queued '%s' for update.", name) the_queue.put_nowait((wc, wc.update, kw)) diff --git a/src/mxdev/vcs/darcs.py b/src/mxdev/vcs/darcs.py index 449fb57..9f56a21 100755 --- a/src/mxdev/vcs/darcs.py +++ b/src/mxdev/vcs/darcs.py @@ -13,11 +13,11 @@ class DarcsError(common.WCError): class DarcsWorkingCopy(common.BaseWorkingCopy): - def __init__(self, source: typing.Dict[str, typing.Any]): + def __init__(self, source: dict[str, typing.Any]): super().__init__(source) self.darcs_executable = common.which("darcs") - def darcs_checkout(self, **kwargs) -> typing.Union[str, None]: + def darcs_checkout(self, **kwargs) -> str | None: name = self.source["name"] path = self.source["path"] url = self.source["url"] @@ -37,10 +37,10 @@ def darcs_checkout(self, **kwargs) -> typing.Union[str, None]: return stdout.decode("utf8") return None - def darcs_update(self, **kwargs) -> typing.Union[str, None]: + def darcs_update(self, **kwargs) -> str | None: name = self.source["name"] path = self.source["path"] - self.output((logger.info, "Updating '%s' with darcs." % name)) + self.output((logger.info, f"Updating '{name}' with darcs.")) cmd = subprocess.Popen( [self.darcs_executable, "pull", "-a"], cwd=path, @@ -56,7 +56,7 @@ def darcs_update(self, **kwargs) -> typing.Union[str, None]: return stdout.decode("utf8") return None - def checkout(self, **kwargs) -> typing.Union[str, None]: + def checkout(self, **kwargs) -> str | None: name = self.source["name"] path = self.source["path"] update = self.should_update(**kwargs) @@ -113,7 +113,7 @@ def _darcs_related_repositories(self) -> typing.Generator: def matches(self) -> bool: return self.source["url"] in self._darcs_related_repositories() - def status(self, **kwargs) -> typing.Union[str, typing.Tuple[str, str]]: + def status(self, **kwargs) -> str | tuple[str, str]: path = self.source["path"] cmd = subprocess.Popen( [self.darcs_executable, "whatsnew"], @@ -131,12 +131,12 @@ def status(self, **kwargs) -> typing.Union[str, typing.Tuple[str, str]]: return status, stdout.decode("utf8") return status - def update(self, **kwargs) -> typing.Union[str, None]: + def update(self, **kwargs) -> str | None: name = self.source["name"] if not self.matches(): raise DarcsError( - "Can't update package '%s' because it's URL doesn't match." % name + f"Can't update package '{name}' because it's URL doesn't match." ) if self.status() != "clean" and not kwargs.get("force", False): - raise DarcsError("Can't update package '%s' because it's dirty." % name) + raise DarcsError(f"Can't update package '{name}' because it's dirty.") return self.darcs_update(**kwargs) diff --git a/src/mxdev/vcs/filesystem.py b/src/mxdev/vcs/filesystem.py index e6fd172..60c5c66 100644 --- a/src/mxdev/vcs/filesystem.py +++ b/src/mxdev/vcs/filesystem.py @@ -12,7 +12,7 @@ class FilesystemError(common.WCError): class FilesystemWorkingCopy(common.BaseWorkingCopy): - def checkout(self, **kwargs) -> typing.Union[str, None]: + def checkout(self, **kwargs) -> str | None: name = self.source["name"] path = self.source["path"] if os.path.exists(path): @@ -20,20 +20,19 @@ def checkout(self, **kwargs) -> typing.Union[str, None]: self.output( ( logger.info, - "Filesystem package %r doesn't need a checkout." % name, + f"Filesystem package {name!r} doesn't need a checkout.", ) ) else: raise FilesystemError( - "Directory name for existing package %r differs. " - "Expected %r." % (name, self.source["url"]) + "Directory name for existing package {!r} differs. " + "Expected {!r}.".format(name, self.source["url"]) ) else: raise FilesystemError( - "Directory %r for package %r doesn't exist. " + f"Directory {path!r} for package {name!r} doesn't exist. " "Check in the documentation if you need to add/change a 'sources-dir' option in " "your [buildout] section or a 'path' option in [sources]." - % (path, name) ) return "" @@ -49,8 +48,8 @@ def update(self, **kwargs): name = self.source["name"] if not self.matches(): raise FilesystemError( - "Directory name for existing package %r differs. " - "Expected %r." % (name, self.source["url"]) + "Directory name for existing package {!r} differs. " + "Expected {!r}.".format(name, self.source["url"]) ) - self.output((logger.info, "Filesystem package %r doesn't need update." % name)) + self.output((logger.info, f"Filesystem package {name!r} doesn't need update.")) return "" diff --git a/src/mxdev/vcs/git.py b/src/mxdev/vcs/git.py index 72c9703..6f8abe7 100644 --- a/src/mxdev/vcs/git.py +++ b/src/mxdev/vcs/git.py @@ -26,12 +26,12 @@ class GitWorkingCopy(common.BaseWorkingCopy): # should make master and a lot of other conventional stuff configurable _upstream_name = "origin" - def __init__(self, source: typing.Dict[str, str]): + def __init__(self, source: dict[str, str]): self.git_executable = common.which("git") if "rev" in source and "revision" in source: raise ValueError( - "The source definition of '%s' contains " - "duplicate revision options." % source["name"] + "The source definition of '{}' contains " + "duplicate revision options.".format(source["name"]) ) # 'rev' is canonical if "revision" in source: @@ -53,7 +53,7 @@ def __init__(self, source: typing.Dict[str, str]): super().__init__(source) @functools.lru_cache(maxsize=4096) - def git_version(self) -> typing.Tuple[int, ...]: + def git_version(self) -> tuple[int, ...]: cmd = self.run_git(["--version"]) stdout, stderr = cmd.communicate() if cmd.returncode != 0: @@ -91,9 +91,9 @@ def git_version(self) -> typing.Tuple[int, ...]: def _remote_branch_prefix(self): if self.git_version() < (1, 6, 3): return self._upstream_name - return "remotes/%s" % self._upstream_name + return f"remotes/{self._upstream_name}" - def run_git(self, commands: typing.List[str], **kwargs) -> subprocess.Popen: + def run_git(self, commands: list[str], **kwargs) -> subprocess.Popen: commands.insert(0, self.git_executable) kwargs["stdout"] = subprocess.PIPE kwargs["stderr"] = subprocess.PIPE @@ -105,17 +105,17 @@ def run_git(self, commands: typing.List[str], **kwargs) -> subprocess.Popen: def git_merge_rbranch( self, stdout_in: str, stderr_in: str, accept_missing: bool = False - ) -> typing.Tuple[str, str]: + ) -> tuple[str, str]: path = self.source["path"] branch = self.source.get("branch", "master") cmd = self.run_git(["branch", "-a"], cwd=path) stdout, stderr = cmd.communicate() if cmd.returncode != 0: - raise GitError("'git branch -a' failed.\n%s" % stderr) + raise GitError(f"'git branch -a' failed.\n{stderr}") stdout_in += stdout stderr_in += stderr - if not re.search(r"^(\*| ) %s$" % re.escape(branch), stdout, re.M): + if not re.search(rf"^(\*| ) {re.escape(branch)}$", stdout, re.M): # The branch is not local. We should not have reached # this, unless no branch was specified and we guess wrong # that it should be master. @@ -134,19 +134,17 @@ def git_merge_rbranch( ) return stdout_in + stdout, stderr_in + stderr - def git_checkout(self, **kwargs) -> typing.Union[str, None]: + def git_checkout(self, **kwargs) -> str | None: name = self.source["name"] path = str(self.source["path"]) url = self.source["url"] if os.path.exists(path): - self.output( - (logger.info, "Skipped cloning of existing package '%s'." % name) - ) + self.output((logger.info, f"Skipped cloning of existing package '{name}'.")) return None - msg = "Cloned '%s' with git" % name + msg = f"Cloned '{name}' with git" if "branch" in self.source: - msg += " using branch '%s'" % self.source["branch"] - msg += " from '%s'." % url + msg += " using branch '{}'".format(self.source["branch"]) + msg += f" from '{url}'." self.output((logger.info, msg)) args = ["clone", "--quiet"] update_git_submodules = self.source.get("submodules", kwargs["submodules"]) @@ -177,8 +175,7 @@ def git_checkout(self, **kwargs) -> typing.Union[str, None]: self.output( ( logger.info, - "Initialized '%s' submodule at '%s' with git." - % (name, submodule), + f"Initialized '{name}' submodule at '{submodule}' with git.", ) ) @@ -188,7 +185,7 @@ def git_checkout(self, **kwargs) -> typing.Union[str, None]: def git_switch_branch( self, stdout_in: str, stderr_in: str, accept_missing: bool = False - ) -> typing.Tuple[str, str]: + ) -> tuple[str, str]: """Switch branches. If accept_missing is True, we do not switch the branch if it @@ -200,14 +197,16 @@ def git_switch_branch( cmd = self.run_git(["branch", "-a"], cwd=path) stdout, stderr = cmd.communicate() if cmd.returncode != 0: - raise GitError("'git branch -a' failed.\n%s" % stderr) + raise GitError(f"'git branch -a' failed.\n{stderr}") stdout_in += stdout stderr_in += stderr if "rev" in self.source: # A tag or revision was specified instead of a branch argv = ["checkout", self.source["rev"]] - self.output((logger.info, "Switching to rev '%s'." % self.source["rev"])) - elif re.search(r"^(\*| ) %s$" % re.escape(branch), stdout, re.M): + self.output( + (logger.info, "Switching to rev '{}'.".format(self.source["rev"])) + ) + elif re.search(rf"^(\*| ) {re.escape(branch)}$", stdout, re.M): # the branch is local, normal checkout will work argv = ["checkout", branch] self.output((logger.info, f"Switching to branch '{branch}'.")) @@ -244,10 +243,10 @@ def git_is_tag(self, tag_name: str) -> bool: # git tag -l returns the tag name if it exists, empty if not return tag_name in stdout.strip().split("\n") - def git_update(self, **kwargs) -> typing.Union[str, None]: + def git_update(self, **kwargs) -> str | None: name = self.source["name"] path = self.source["path"] - self.output((logger.info, "Updated '%s' with git." % name)) + self.output((logger.info, f"Updated '{name}' with git.")) # First we fetch. This should always be possible. argv = ["fetch", "--tags"] # Also fetch tags explicitly update_git_submodules = self.source.get("submodules", kwargs["submodules"]) @@ -298,8 +297,7 @@ def git_update(self, **kwargs) -> typing.Union[str, None]: self.output( ( logger.info, - "Initialized '%s' submodule at '%s' with git." - % (name, submodule), + f"Initialized '{name}' submodule at '{submodule}' with git.", ) ) @@ -307,7 +305,7 @@ def git_update(self, **kwargs) -> typing.Union[str, None]: return stdout return None - def checkout(self, **kwargs) -> typing.Union[str, None]: + def checkout(self, **kwargs) -> str | None: name = self.source["name"] path = self.source["path"] update = self.should_update(**kwargs) @@ -318,19 +316,20 @@ def checkout(self, **kwargs) -> typing.Union[str, None]: return self.update(**kwargs) elif self.matches(): self.output( - (logger.info, "Skipped checkout of existing package '%s'." % name) + (logger.info, f"Skipped checkout of existing package '{name}'.") ) else: self.output( ( logger.warning, - "Checkout URL for existing package '%s' differs. Expected '%s'." - % (name, self.source["url"]), + "Checkout URL for existing package '{}' differs. Expected '{}'.".format( + name, self.source["url"] + ), ) ) return None - def status(self, **kwargs) -> typing.Union[typing.Tuple[str, str], str]: + def status(self, **kwargs) -> tuple[str, str] | str: path = self.source["path"] cmd = self.run_git(["status", "-s", "-b"], cwd=path) stdout, stderr = cmd.communicate() @@ -357,24 +356,24 @@ def matches(self) -> bool: raise GitError(f"git remote of '{name}' failed.\n{stderr}") return self.source["url"] in stdout.split() - def update(self, **kwargs) -> typing.Union[str, None]: + def update(self, **kwargs) -> str | None: name = self.source["name"] if not self.matches(): self.output( ( logger.warning, - "Can't update package '%s' because its URL doesn't match." % name, + f"Can't update package '{name}' because its URL doesn't match.", ) ) if self.status() != "clean" and not kwargs.get("force", False): - raise GitError("Can't update package '%s' because it's dirty." % name) + raise GitError(f"Can't update package '{name}' because it's dirty.") return self.git_update(**kwargs) - def git_set_pushurl(self, stdout_in, stderr_in) -> typing.Tuple[str, str]: + def git_set_pushurl(self, stdout_in, stderr_in) -> tuple[str, str]: cmd = self.run_git( [ "config", - "remote.%s.pushurl" % self._upstream_name, + f"remote.{self._upstream_name}.pushurl", self.source["pushurl"], ], cwd=self.source["path"], @@ -383,14 +382,13 @@ def git_set_pushurl(self, stdout_in, stderr_in) -> typing.Tuple[str, str]: if cmd.returncode != 0: raise GitError( - "git config remote.%s.pushurl %s \nfailed.\n" - % (self._upstream_name, self.source["pushurl"]) + "git config remote.{}.pushurl {} \nfailed.\n".format( + self._upstream_name, self.source["pushurl"] + ) ) return (stdout_in + stdout, stderr_in + stderr) - def git_init_submodules( - self, stdout_in, stderr_in - ) -> typing.Tuple[str, str, typing.List]: + def git_init_submodules(self, stdout_in, stderr_in) -> tuple[str, str, list]: cmd = self.run_git(["submodule", "init"], cwd=self.source["path"]) stdout, stderr = cmd.communicate() if cmd.returncode != 0: @@ -403,7 +401,7 @@ def git_init_submodules( def git_update_submodules( self, stdout_in, stderr_in, submodule="all", recursive: bool = False - ) -> typing.Tuple[str, str]: + ) -> tuple[str, str]: params = ["submodule", "update"] if recursive: params.append("--init") diff --git a/src/mxdev/vcs/gitsvn.py b/src/mxdev/vcs/gitsvn.py index 6db68b3..f8422eb 100644 --- a/src/mxdev/vcs/gitsvn.py +++ b/src/mxdev/vcs/gitsvn.py @@ -19,7 +19,7 @@ def __init__(self, source): def gitify_init(self, **kwargs): name = self.source["name"] path = self.source["path"] - self.output((logger.info, "Gitified '%s'." % name)) + self.output((logger.info, f"Gitified '{name}'.")) cmd = subprocess.Popen( [self.gitify_executable, "init"], cwd=path, @@ -43,7 +43,7 @@ def svn_switch(self, **kwargs): def svn_update(self, **kwargs): name = self.source["name"] path = self.source["path"] - self.output((logger.info, "Updated '%s' with gitify." % name)) + self.output((logger.info, f"Updated '{name}' with gitify.")) cmd = subprocess.Popen( [self.gitify_executable, "update"], cwd=path, diff --git a/src/mxdev/vcs/mercurial.py b/src/mxdev/vcs/mercurial.py index 6d818ec..c0891db 100644 --- a/src/mxdev/vcs/mercurial.py +++ b/src/mxdev/vcs/mercurial.py @@ -25,10 +25,10 @@ def hg_clone(self, **kwargs): url = self.source["url"] if os.path.exists(path): - self.output((logger.info, "Skipped cloning of existing package %r." % name)) + self.output((logger.info, f"Skipped cloning of existing package {name!r}.")) return rev = self.get_rev() - self.output((logger.info, "Cloned %r with mercurial." % name)) + self.output((logger.info, f"Cloned {name!r} with mercurial.")) env = dict(os.environ) env.pop("PYTHONPATH", None) cmd = subprocess.Popen( @@ -143,7 +143,7 @@ def hg_pull(self, **kwargs): # However the 'rev' parameter works differently and forces revision name = self.source["name"] path = self.source["path"] - self.output((logger.info, "Updated %r with mercurial." % name)) + self.output((logger.info, f"Updated {name!r} with mercurial.")) env = dict(os.environ) env.pop("PYTHONPATH", None) cmd = subprocess.Popen( @@ -175,12 +175,12 @@ def checkout(self, **kwargs): self.update(**kwargs) elif self.matches(): self.output( - (logger.info, "Skipped checkout of existing package %r." % name) + (logger.info, f"Skipped checkout of existing package {name!r}.") ) else: raise MercurialError( - "Source URL for existing package %r differs. " - "Expected %r." % (name, self.source["url"]) + "Source URL for existing package {!r} differs. " + "Expected {!r}.".format(name, self.source["url"]) ) else: return self.hg_clone(**kwargs) @@ -236,8 +236,8 @@ def update(self, **kwargs): name = self.source["name"] if not self.matches(): raise MercurialError( - "Can't update package %r because its URL doesn't match." % name + f"Can't update package {name!r} because its URL doesn't match." ) if self.status() != "clean" and not kwargs.get("force", False): - raise MercurialError("Can't update package %r because it's dirty." % name) + raise MercurialError(f"Can't update package {name!r} because it's dirty.") return self.hg_pull(**kwargs) diff --git a/src/mxdev/vcs/svn.py b/src/mxdev/vcs/svn.py index 0856975..4430500 100644 --- a/src/mxdev/vcs/svn.py +++ b/src/mxdev/vcs/svn.py @@ -34,9 +34,9 @@ class SVNCertificateRejectedError(SVNError): class SVNWorkingCopy(common.BaseWorkingCopy): - _svn_info_cache: typing.Dict = {} - _svn_auth_cache: typing.Dict = {} - _svn_cert_cache: typing.Dict = {} + _svn_info_cache: dict = {} + _svn_auth_cache: dict = {} + _svn_cert_cache: dict = {} @classmethod def _clear_caches(klass): @@ -53,13 +53,15 @@ def _normalized_url_rev(self): url[2] = path if "rev" in self.source and "revision" in self.source: raise ValueError( - "The source definition of '%s' contains duplicate revision options." - % self.source["name"] + "The source definition of '{}' contains duplicate revision options.".format( + self.source["name"] + ) ) if rev is not None and ("rev" in self.source or "revision" in self.source): raise ValueError( - "The url of '%s' contains a revision and there is an additional revision option." - % self.source["name"] + "The url of '{}' contains a revision and there is an additional revision option.".format( + self.source["name"] + ) ) elif rev is None: rev = self.source.get("revision", self.source.get("rev")) @@ -97,14 +99,15 @@ def _svn_check_version(self): if (cmd.returncode != 0) or (version is None): logger.error("Couldn't determine the version of 'svn' command.") logger.error( - "Subversion output:\n%s\n%s" % stdout.decode("utf8"), + "Subversion output:\n{}\n{}".format(*stdout.decode("utf8")), stderr.decode("utf8"), ) sys.exit(1) if (version < (1, 5)) and not _svn_version_warning: logger.warning( - "The installed 'svn' command is too old. Expected 1.5 or newer, got %s." - % ".".join([str(x) for x in version]) + "The installed 'svn' command is too old. Expected 1.5 or newer, got {}.".format( + ".".join([str(x) for x in version]) + ) ) _svn_version_warning = True @@ -137,8 +140,9 @@ def _svn_error_wrapper(self, f, **kwargs): common.output_lock.release() continue print( - "Authorization needed for '%s' at '%s'" - % (self.source["name"], self.source["url"]) + "Authorization needed for '{}' at '{}'".format( + self.source["name"], self.source["url"] + ) ) user = input("Username: ") passwd = getpass.getpass("Password: ") @@ -185,8 +189,9 @@ def _svn_checkout(self, **kwargs): stdout, stderr, returncode = self._svn_communicate(args, url, **kwargs) if returncode != 0: raise SVNError( - "Subversion checkout for '%s' failed.\n%s" - % (name, stderr.decode("utf8")) + "Subversion checkout for '{}' failed.\n{}".format( + name, stderr.decode("utf8") + ) ) if kwargs.get("verbose", False): return stdout.decode("utf8") @@ -272,7 +277,7 @@ def _svn_switch(self, **kwargs): url, rev = self._normalized_url_rev() args = [self.svn_executable, "switch", url, path] if rev is not None and not rev.startswith(">"): - args.insert(2, "-r%s" % rev) + args.insert(2, f"-r{rev}") stdout, stderr, returncode = self._svn_communicate(args, url, **kwargs) if returncode != 0: raise SVNError( @@ -289,7 +294,7 @@ def _svn_update(self, **kwargs): url, rev = self._normalized_url_rev() args = [self.svn_executable, "update", path] if rev is not None and not rev.startswith(">"): - args.insert(2, "-r%s" % rev) + args.insert(2, f"-r{rev}") stdout, stderr, returncode = self._svn_communicate(args, url, **kwargs) if returncode != 0: raise SVNError( @@ -305,20 +310,20 @@ def svn_checkout(self, **kwargs): path = self.source["path"] if os.path.exists(path): self.output( - (logger.info, "Skipped checkout of existing package '%s'." % name) + (logger.info, f"Skipped checkout of existing package '{name}'.") ) return - self.output((logger.info, "Checked out '%s' with subversion." % name)) + self.output((logger.info, f"Checked out '{name}' with subversion.")) return self._svn_error_wrapper(self._svn_checkout, **kwargs) def svn_switch(self, **kwargs): name = self.source["name"] - self.output((logger.info, "Switched '%s' with subversion." % name)) + self.output((logger.info, f"Switched '{name}' with subversion.")) return self._svn_error_wrapper(self._svn_switch, **kwargs) def svn_update(self, **kwargs): name = self.source["name"] - self.output((logger.info, "Updated '%s' with subversion." % name)) + self.output((logger.info, f"Updated '{name}' with subversion.")) return self._svn_error_wrapper(self._svn_update, **kwargs) def checkout(self, **kwargs): @@ -334,7 +339,7 @@ def checkout(self, **kwargs): self.output( ( logger.info, - "Skipped checkout of existing package '%s'." % name, + f"Skipped checkout of existing package '{name}'.", ) ) else: @@ -344,13 +349,14 @@ def checkout(self, **kwargs): url = self._svn_info().get("url", "") if url: msg = f"The current checkout of '{name}' is from '{url}'." - msg += "\nCan't switch package to '%s' because it's dirty." % ( - self.source["url"] + msg += ( + "\nCan't switch package to '{}' because it's dirty.".format( + self.source["url"] + ) ) else: - msg = ( - "Can't switch package '%s' to '%s' because it's dirty." - % (name, self.source["url"]) + msg = "Can't switch package '{}' to '{}' because it's dirty.".format( + name, self.source["url"] ) raise SVNError(msg) else: @@ -416,7 +422,7 @@ def update(self, **kwargs): if force or status == "clean": return self.svn_switch(**kwargs) else: - raise SVNError("Can't switch package '%s' because it's dirty." % name) + raise SVNError(f"Can't switch package '{name}' because it's dirty.") if status != "clean" and not force: - raise SVNError("Can't update package '%s' because it's dirty." % name) + raise SVNError(f"Can't update package '{name}' because it's dirty.") return self.svn_update(**kwargs) diff --git a/tests/test_common.py b/tests/test_common.py index 76a2d35..54f7665 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -27,16 +27,16 @@ def test_BaseWorkingCopy(): common.BaseWorkingCopy(source={}) class TestWorkingCopy(common.BaseWorkingCopy): - def checkout(self, **kwargs) -> typing.Union[str, None]: # type: ignore + def checkout(self, **kwargs) -> str | None: # type: ignore ... - def status(self, **kwargs) -> typing.Union[typing.Tuple[str, str], str]: # type: ignore + def status(self, **kwargs) -> tuple[str, str] | str: # type: ignore ... def matches(self) -> bool: # type: ignore ... - def update(self, **kwargs) -> typing.Union[str, None]: # type: ignore + def update(self, **kwargs) -> str | None: # type: ignore ... bwc = TestWorkingCopy(source=dict(url="https://tld.com/repo.git")) @@ -154,17 +154,17 @@ def __call__(self, code): class TestWorkingCopy(common.BaseWorkingCopy): package_status = "clean" - def checkout(self, **kwargs) -> typing.Union[str, None]: + def checkout(self, **kwargs) -> str | None: common.logger.info(f"Checkout called with: {kwargs}") return None - def status(self, **kwargs) -> typing.Union[typing.Tuple[str, str], str]: + def status(self, **kwargs) -> tuple[str, str] | str: return self.package_status def matches(self) -> bool: # type: ignore ... - def update(self, **kwargs) -> typing.Union[str, None]: # type: ignore + def update(self, **kwargs) -> str | None: # type: ignore ... class WCT(dict): diff --git a/tests/test_git.py b/tests/test_git.py index d43ede3..935dceb 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -159,7 +159,7 @@ def test_update_without_revision_pin(mkgitrepo, src, capsys, caplog): path / "foo", } assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % repository.url,), {}), + ("info", (f"Cloned 'egg' with git from '{repository.url}'.",), {}), ("info", ("Updated 'egg' with git.",), {}), ("info", ("Switching to remote branch 'master'.",), {}), ] @@ -193,7 +193,7 @@ def test_update_verbose(mkgitrepo, src, capsys): path / "foo", } assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % repository.url,), {}), + ("info", (f"Cloned 'egg' with git from '{repository.url}'.",), {}), ("info", ("Updated 'egg' with git.",), {}), ("info", ("Switching to remote branch 'master'.",), {}), ] diff --git a/tests/test_git_submodules.py b/tests/test_git_submodules.py index 3e5b071..36386b1 100644 --- a/tests/test_git_submodules.py +++ b/tests/test_git_submodules.py @@ -38,10 +38,10 @@ def test_checkout_with_submodule(mkgitrepo, src, caplog, git_allow_file_protocol log.method_calls == log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_name,), + (f"Initialized 'egg' submodule at '{submodule_name}' with git.",), {}, ), ] @@ -86,15 +86,15 @@ def test_checkout_with_two_submodules(mkgitrepo, src, git_allow_file_protocol): "foo_b", } assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_name,), + (f"Initialized 'egg' submodule at '{submodule_name}' with git.",), {}, ), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_b_name,), + (f"Initialized 'egg' submodule at '{submodule_b_name}' with git.",), {}, ), ] @@ -141,7 +141,7 @@ def test_checkout_with_two_submodules_recursive( "foo_b", } assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ] @@ -171,10 +171,10 @@ def test_update_with_submodule(mkgitrepo, src, git_allow_file_protocol): } assert set(os.listdir(src / "egg" / submodule_name)) == {".git", "foo"} assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_name,), + (f"Initialized 'egg' submodule at '{submodule_name}' with git.",), {}, ), ] @@ -202,7 +202,7 @@ def test_update_with_submodule(mkgitrepo, src, git_allow_file_protocol): ("info", ("Switching to branch 'master'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_b_name,), + (f"Initialized 'egg' submodule at '{submodule_b_name}' with git.",), {}, ), ] @@ -234,7 +234,7 @@ def test_update_with_submodule_recursive(mkgitrepo, src, git_allow_file_protocol } assert set(os.listdir(src / "egg" / submodule_name)) == {".git", "foo"} assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ] submodule_b_name = "submodule_b" @@ -293,7 +293,7 @@ def test_checkout_with_submodules_option_never(mkgitrepo, src, git_allow_file_pr } assert set(os.listdir(src / "egg" / submodule_name)) == set() assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}) + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}) ] @@ -350,13 +350,13 @@ def test_checkout_with_submodules_option_never_source_always( assert set(os.listdir(src / "egg2" / submodule_name)) == set() assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_name,), + (f"Initialized 'egg' submodule at '{submodule_name}' with git.",), {}, ), - ("info", ("Cloned 'egg2' with git from '%s'." % egg2.url,), {}), + ("info", (f"Cloned 'egg2' with git from '{egg2.url}'.",), {}), ] @@ -412,13 +412,13 @@ def test_checkout_with_submodules_option_always_source_never( assert set(os.listdir(src / "egg2" / submodule_name)) == set() assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_name,), + (f"Initialized 'egg' submodule at '{submodule_name}' with git.",), {}, ), - ("info", ("Cloned 'egg2' with git from '%s'." % egg2.url,), {}), + ("info", (f"Cloned 'egg2' with git from '{egg2.url}'.",), {}), ] @@ -457,10 +457,10 @@ def test_update_with_submodule_checkout(mkgitrepo, src, git_allow_file_protocol) } assert set(os.listdir(src / "egg" / submodule_name)) == {".git", "foo"} assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_name,), + (f"Initialized 'egg' submodule at '{submodule_name}' with git.",), {}, ), ] @@ -516,10 +516,10 @@ def test_update_with_submodule_dont_update_previous_submodules( } assert set(os.listdir(src / "egg" / submodule_name)) == {".git", "foo"} assert log.method_calls == [ - ("info", ("Cloned 'egg' with git from '%s'." % egg.url,), {}), + ("info", (f"Cloned 'egg' with git from '{egg.url}'.",), {}), ( "info", - ("Initialized 'egg' submodule at '%s' with git." % submodule_name,), + (f"Initialized 'egg' submodule at '{submodule_name}' with git.",), {}, ), ] diff --git a/tests/test_mercurial.py b/tests/test_mercurial.py index 77d1ffc..438edf1 100644 --- a/tests/test_mercurial.py +++ b/tests/test_mercurial.py @@ -15,21 +15,21 @@ def testUpdateWithoutRevisionPin(self, develop, src, tempdir): repository = tempdir["repository"] os.mkdir(repository) process = Process(cwd=repository) - process.check_call("hg init %s" % repository) + process.check_call(f"hg init {repository}") foo = repository["foo"] foo.create_file("foo") - process.check_call("hg add %s" % foo, echo=False) - process.check_call("hg commit %s -m foo -u test" % foo, echo=False) + process.check_call(f"hg add {foo}", echo=False) + process.check_call(f"hg commit {foo} -m foo -u test", echo=False) bar = repository["bar"] bar.create_file("bar") - process.check_call("hg add %s" % bar, echo=False) - process.check_call("hg commit %s -m bar -u test" % bar, echo=False) + process.check_call(f"hg add {bar}", echo=False) + process.check_call(f"hg commit {bar} -m bar -u test", echo=False) develop.sources = { "egg": dict( kind="hg", name="egg", - url="%s" % repository, + url=f"{repository}", path=os.path.join(src, "egg"), ) } @@ -56,18 +56,18 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): repository = tempdir["repository"] os.mkdir(repository) process = Process(cwd=repository) - lines = process.check_call("hg init %s" % repository) + lines = process.check_call(f"hg init {repository}") foo = repository["foo"] foo.create_file("foo") - lines = process.check_call("hg add %s" % foo, echo=False) + lines = process.check_call(f"hg add {foo}", echo=False) # create branch for testing lines = process.check_call("hg branch test", echo=False) - lines = process.check_call("hg commit %s -m foo -u test" % foo, echo=False) + lines = process.check_call(f"hg commit {foo} -m foo -u test", echo=False) # get comitted rev - lines = process.check_call("hg log %s" % foo, echo=False) + lines = process.check_call(f"hg log {foo}", echo=False) try: # XXX older version @@ -80,8 +80,8 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): bar = repository["bar"] bar.create_file("bar") - lines = process.check_call("hg add %s" % bar, echo=False) - lines = process.check_call("hg commit %s -m bar -u test" % bar, echo=False) + lines = process.check_call(f"hg add {bar}", echo=False) + lines = process.check_call(f"hg commit {bar} -m bar -u test", echo=False) # check rev develop.sources = { @@ -89,7 +89,7 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): kind="hg", name="egg", rev=rev, - url="%s" % repository, + url=f"{repository}", path=os.path.join(src, "egg"), ) } @@ -104,7 +104,7 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): kind="hg", name="egg", branch="test", - url="%s" % repository, + url=f"{repository}", path=os.path.join(src, "egg"), ) } @@ -121,7 +121,7 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): name="egg", branch="test", rev=rev, - url="%s" % repository, + url=f"{repository}", path=os.path.join(src, "egg-failed"), ) } diff --git a/tests/test_svn.py b/tests/test_svn.py index d9a379a..236fee3 100644 --- a/tests/test_svn.py +++ b/tests/test_svn.py @@ -20,20 +20,20 @@ def testUpdateWithoutRevisionPin(self, develop, src, tempdir): process = Process() repository = tempdir["repository"] - process.check_call("svnadmin create %s" % repository) + process.check_call(f"svnadmin create {repository}") checkout = tempdir["checkout"] process.check_call(f"svn checkout file://{repository} {checkout}", echo=False) foo = checkout["foo"] foo.create_file("foo") - process.check_call("svn add %s" % foo, echo=False) - process.check_call("svn commit %s -m foo" % foo, echo=False) + process.check_call(f"svn add {foo}", echo=False) + process.check_call(f"svn commit {foo} -m foo", echo=False) bar = checkout["bar"] bar.create_file("bar") - process.check_call("svn add %s" % bar, echo=False) - process.check_call("svn commit %s -m bar" % bar, echo=False) + process.check_call(f"svn add {bar}", echo=False) + process.check_call(f"svn commit {bar} -m bar", echo=False) develop.sources = { "egg": dict( - kind="svn", name="egg", url="file://%s" % repository, path=src["egg"] + kind="svn", name="egg", url=f"file://{repository}", path=src["egg"] ) } _log = patch("mxdev.vcs.svn.logger") @@ -57,20 +57,20 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): process = Process() repository = tempdir["repository"] - process.check_call("svnadmin create %s" % repository) + process.check_call(f"svnadmin create {repository}") checkout = tempdir["checkout"] process.check_call(f"svn checkout file://{repository} {checkout}", echo=False) foo = checkout["foo"] foo.create_file("foo") - process.check_call("svn add %s" % foo, echo=False) - process.check_call("svn commit %s -m foo" % foo, echo=False) + process.check_call(f"svn add {foo}", echo=False) + process.check_call(f"svn commit {foo} -m foo", echo=False) bar = checkout["bar"] bar.create_file("bar") - process.check_call("svn add %s" % bar, echo=False) - process.check_call("svn commit %s -m bar" % bar, echo=False) + process.check_call(f"svn add {bar}", echo=False) + process.check_call(f"svn commit {bar} -m bar", echo=False) develop.sources = { "egg": dict( - kind="svn", name="egg", url="file://%s@1" % repository, path=src["egg"] + kind="svn", name="egg", url=f"file://{repository}@1", path=src["egg"] ) } CmdCheckout(develop)(develop.parser.parse_args(["co", "egg"])) diff --git a/tests/utils.py b/tests/utils.py index 5d34146..582c516 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,9 +2,7 @@ from subprocess import PIPE from subprocess import Popen from typing import Any -from typing import Dict -from typing import Iterable -from typing import Union +from collections.abc import Iterable import os import sys @@ -121,7 +119,7 @@ def __call__(self, line): class Process: """Process related functions using the tee module.""" - def __init__(self, quiet: bool = False, env=None, cwd: Union[str, None] = None): + def __init__(self, quiet: bool = False, env=None, cwd: str | None = None): self.quiet = quiet self.env = env self.cwd = cwd @@ -159,7 +157,7 @@ def add_file(self, fname, msg=None): repo_file = self.base / fname with open(repo_file, "w") as fio: fio.write(fname) - self("git add %s" % repo_file, echo=False) + self(f"git add {repo_file}", echo=False) if msg is None: msg = fname self(f"git commit {repo_file} -m {msg}", echo=False) @@ -173,12 +171,12 @@ def add_submodule(self, submodule: "GitRepo", submodule_name: str): self(f"git add {submodule_name}") self(f"git commit -m 'Add submodule {submodule_name}'") - def add_branch(self, bname: str, msg: Union[str, None] = None): + def add_branch(self, bname: str, msg: str | None = None): self(f"git checkout -b {bname}") def vcs_checkout( - sources: Dict[str, Any], + sources: dict[str, Any], packages: Iterable[str], verbose, update_git_submodules: str = "always", @@ -196,7 +194,7 @@ def vcs_checkout( def vcs_update( - sources: Dict[str, Any], + sources: dict[str, Any], packages: Iterable[str], verbose, update_git_submodules: str = "always", @@ -213,7 +211,7 @@ def vcs_update( ) -def vcs_status(sources: Dict[str, Any], verbose=False): +def vcs_status(sources: dict[str, Any], verbose=False): workingcopies = WorkingCopies(sources=sources, threads=1) res = {} for k in sources: