From 6f5ba74f4948562d27bb1cf0fc8c1816c71045d8 Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Mon, 6 May 2024 17:08:43 +0200 Subject: [PATCH] Remove setup.cfg, switch to pyproject.toml, switch from FLake8 to Ruff --- .github/workflows/release.yml | 12 ++++---- Makefile | 16 ++++++++++ pyproject.toml | 42 ++++++++++++++++++++++++++ qgis_plugin_manager/__about__.py | 2 +- qgis_plugin_manager/__main__.py | 8 ++--- qgis_plugin_manager/definitions.py | 4 +-- qgis_plugin_manager/local_directory.py | 18 +++++------ qgis_plugin_manager/remote.py | 34 ++++++++++----------- qgis_plugin_manager/utils.py | 12 ++++---- requirements-dev.txt | 1 + setup.cfg | 22 -------------- setup.py | 4 +-- test/test_full_install.py | 8 ++--- test/test_local.py | 2 +- test/test_remote.py | 18 +++++------ test/test_utils.py | 6 ++-- 16 files changed, 123 insertions(+), 86 deletions(-) create mode 100644 Makefile create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt delete mode 100644 setup.cfg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f05fb46..4a5311b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,8 +42,8 @@ jobs: export PYTHONPATH="${{ github.workspace }}" python -m unittest - flake8: - name: "❄ Flake8" + ruff: + name: "❄ Ruff" runs-on: ubuntu-latest steps: @@ -59,15 +59,15 @@ jobs: uses: actions/checkout@v4 - name: Install Python requirements - run: pip install flake8 + run: pip install -r requirements.txt - - name: Run flake8 - run: flake8 + - name: Run Ruff + run: make lint release: name: "🚀 Release" runs-on: ubuntu-20.04 - needs: [tests, flake8] + needs: [tests, ruff] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') steps: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3630c95 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: test + +PYTHON_PKG=qgis_plugin_manager +TESTDIR=test + +test: + cd test && python3 -m unittest + +lint: + @ruff check $(PYTHON_PKG) $(TESTDIR) + +lint-preview: + @ruff check --preview $(PYTHON_PKG) $(TESTDIR) + +lint-fix: + @ruff check --fix --preview $(PYTHON_PKG) $(TESTDIR) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cd00f95 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[tool.setuptools] +license-files = "LICENSE" + +# Ruff configuration +# See https://doc.astral.sh/ruff/configuration + +[tool.ruff] +line-length = 110 +target-version = "py37" +exclude = [ + ".venv", + ".local", +] + +[tool.ruff.format] +indent-style = "space" + +[tool.ruff.lint] +extend-select = ["E", "F", "I", "ANN", "W", "T", "COM", "RUF"] +ignore = [ + "ANN101", + "ANN102", + "ANN204", + "T201", +] + +[tool.ruff.lint.per-file-ignores] +"test/*" = [ + "ANN201", +] + +[tool.ruff.lint.isort] +lines-between-types = 1 +known-third-party = [ + "qgis", +] +order-by-type = true + +[tool.ruff.lint.flake8-annotations] +#ignore-fully-untyped = true +suppress-none-returning = true +#suppress-dummy-args = true \ No newline at end of file diff --git a/qgis_plugin_manager/__about__.py b/qgis_plugin_manager/__about__.py index ae7f43c..30dcc1e 100644 --- a/qgis_plugin_manager/__about__.py +++ b/qgis_plugin_manager/__about__.py @@ -39,5 +39,5 @@ [ int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".") - ] + ], ) diff --git a/qgis_plugin_manager/__main__.py b/qgis_plugin_manager/__main__.py index 131f9a5..ca3e5ff 100755 --- a/qgis_plugin_manager/__main__.py +++ b/qgis_plugin_manager/__main__.py @@ -16,15 +16,15 @@ from qgis_plugin_manager.utils import qgis_server_version -def main() -> int: # noqa: C901 +def main() -> int: """ Main function for the CLI menu. """ parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter + formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("-v", "--version", action="version", version=__version__) subparsers = parser.add_subparsers( - title="commands", description="qgis-plugin-manager command", dest="command" + title="commands", description="qgis-plugin-manager command", dest="command", ) subparsers.add_parser("init", help="Create the `sources.list` with plugins.qgis.org as remote") @@ -167,7 +167,7 @@ def main() -> int: # noqa: C901 f"{Level.Alert}" f"Plugin {plugin_object.name} skipped from the upgrade command, because the plugin " f"is in the ignored list." - f"{Level.End}" + f"{Level.End}", ) continue diff --git a/qgis_plugin_manager/definitions.py b/qgis_plugin_manager/definitions.py index a813a11..056dc8e 100644 --- a/qgis_plugin_manager/definitions.py +++ b/qgis_plugin_manager/definitions.py @@ -1,4 +1,4 @@ -from typing import NamedTuple +from typing import ClassVar, NamedTuple __copyright__ = 'Copyright 2021, 3Liz' __license__ = 'GPL version 3' @@ -10,7 +10,7 @@ class Plugin(NamedTuple): name: str = None description: str = None version: str = None - search: list = [] + search: ClassVar = [] qgis_minimum_version: str = None qgis_maximum_version: str = None homepage: str = None diff --git a/qgis_plugin_manager/local_directory.py b/qgis_plugin_manager/local_directory.py index b7e052f..f3157d9 100644 --- a/qgis_plugin_manager/local_directory.py +++ b/qgis_plugin_manager/local_directory.py @@ -8,7 +8,7 @@ import stat from pathlib import Path -from typing import Dict, Union +from typing import Dict, Optional, Union from qgis_plugin_manager.definitions import Level, Plugin from qgis_plugin_manager.remote import Remote @@ -25,7 +25,7 @@ class LocalDirectory: - def __init__(self, folder: Path, qgis_version: str = None): + def __init__(self, folder: Path, qgis_version: Optional[str] = None): """ Constructor""" self.folder = folder # Dictionary : folder : plugin name @@ -55,7 +55,7 @@ def init(self) -> bool: print( f"{Level.Alert}" f"QGIS version is unknown, creating with a default {DEFAULT_QGIS_VERSION}" - f"{Level.End}" + f"{Level.End}", ) version = DEFAULT_QGIS_VERSION @@ -152,7 +152,7 @@ def plugin_info(self, plugin: str) -> Union[None, Plugin]: ) return data - def plugin_installed_version(self, plugin_name) -> Union[str, None]: + def plugin_installed_version(self, plugin_name: str) -> Union[str, None]: """ If a plugin is installed or not. """ if self._plugins is None: self.plugin_list() @@ -182,7 +182,7 @@ def remove(self, plugin_name: str) -> bool: try: shutil.rmtree(plugin_path) except Exception as e: - print(f"{Level.Critical}Plugin {plugin_name} could not be removed : {str(e)}") + print(f"{Level.Critical}Plugin {plugin_name} could not be removed : {e!s}") if not Path(self.folder.joinpath(plugin_folder)).exists(): print(f"{Level.Success}Plugin {plugin_name} removed") @@ -193,7 +193,7 @@ def remove(self, plugin_name: str) -> bool: f"{Level.Alert}" f"Plugin {plugin_name} using folder {plugin_folder} could not be removed " f"for unknown reason" - f"{Level.End}" + f"{Level.End}", ) break print(f"{Level.Alert}Plugin name '{plugin_name}' not found{Level.End}") @@ -205,7 +205,7 @@ def remove(self, plugin_name: str) -> bool: return False - def print_table(self, current_directory: bool = True): # noqa: C901 + def print_table(self, current_directory: bool = True): """ Print all plugins installed as a table. """ if self._plugins is None: self.plugin_list() @@ -322,7 +322,7 @@ def print_table(self, current_directory: bool = True): # noqa: C901 print(pretty_table(data, headers)) else: print( - f"{Level.Alert}No plugin found in the current directory {self.folder.absolute()}{Level.End}" + f"{Level.Alert}No plugin found in the current directory {self.folder.absolute()}{Level.End}", ) if len(list_of_owners) > 1: @@ -331,7 +331,7 @@ def print_table(self, current_directory: bool = True): # noqa: C901 f"{Level.Alert}" f"Different rights have been detected : {','.join(list_of_owners)}" f"{Level.End}. " - f"Please check user-rights." + f"Please check user-rights.", ) if len(self._invalid) >= 1: diff --git a/qgis_plugin_manager/remote.py b/qgis_plugin_manager/remote.py index 8844135..c898f21 100644 --- a/qgis_plugin_manager/remote.py +++ b/qgis_plugin_manager/remote.py @@ -11,8 +11,8 @@ import zipfile from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union -from urllib.parse import unquote, urlencode, urlparse, urlunparse, parse_qs +from typing import Dict, Iterator, List, Optional, Tuple, Union +from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse from xml.etree.ElementTree import parse from qgis_plugin_manager.definitions import Level, Plugin @@ -27,7 +27,7 @@ class Remote: - def __init__(self, folder: Path, qgis_version: str = None): + def __init__(self, folder: Path, qgis_version: Optional[str] = None): """ Constructor. """ self.folder = folder self.list = None @@ -61,7 +61,7 @@ def remote_is_ready(self) -> bool: f"{Level.Critical}" f"The 'update' command has not been done before. " f"The repository {server} has not been fetched before." - f"{Level.End}" + f"{Level.End}", ) self.setting_error = True return False @@ -96,7 +96,7 @@ def remote_list(self) -> list: print( f"{Level.Alert}" f"Your https://plugins.qgis.org remote is not using a dynamic QGIS version." - f"{Level.End}" + f"{Level.End}", ) print( f"Instead of\n'{raw_line}'" @@ -108,7 +108,7 @@ def remote_list(self) -> list: f"regenerate it using dynamic QGIS version if QGIS is well configured.\n" f"This is only a warning, the process will continue with the hardcoded QGIS " f"version in your 'sources.list' file." - f"\n\n" + f"\n\n", ) if "[VERSION]" in raw_line: @@ -117,7 +117,7 @@ def remote_list(self) -> list: f"{Level.Alert}" f"Skipping line '{raw_line}' because it has a token [VERSION] but " f"no QGIS version could be detected." - f"{Level.End}" + f"{Level.End}", ) continue @@ -298,7 +298,7 @@ def _parse_xml(self, xml_file: Path, plugins: Dict) -> Dict: self.list_plugins[xml_plugin_name] = plugin_obj return plugins - def search(self, search_string: str, strict=True) -> List: + def search(self, search_string: str, strict: bool = True) -> List: """ Search in plugin names and tags.""" # strict is used in tests to not check if the remote is ready if strict and not self.remote_is_ready(): @@ -319,8 +319,8 @@ def search(self, search_string: str, strict=True) -> List: return results def install( - self, plugin_name, version="latest", current_version: str = "", force: bool = False, - remove_zip=True + self, plugin_name: str, version: str = "latest", current_version: str = "", force: bool = False, + remove_zip: bool = True, ) -> bool: """ Install the plugin with a specific version. @@ -349,7 +349,7 @@ def install( if current_version == actual: if not force: print( - f"\t{Level.Alert}Same version detected on the remote, skipping {plugin_name}{Level.End}" + f"\t{Level.Alert}Same version detected on the remote, skipping {plugin_name}{Level.End}", ) # Plugin is installed and correct version, it's exit code 0 return True @@ -405,7 +405,7 @@ def install( return True def _download_zip( - self, url: str, version: str, plugin_name: str, file_name: str, user: str + self, url: str, version: str, plugin_name: str, file_name: str, user: str, ) -> Tuple[bool, Union[None, Path]]: """ Download the ZIP """ @@ -457,7 +457,7 @@ def _download_zip( return True, zip_file @staticmethod - def check_qgis_dev_version(qgis_version) -> Optional[List[str]]: + def check_qgis_dev_version(qgis_version: str) -> Optional[List[str]]: """ Check if the QGIS current version is odd number. """ if not qgis_version: return None @@ -467,18 +467,18 @@ def check_qgis_dev_version(qgis_version) -> Optional[List[str]]: print( f"{Level.Alert}" f"A QGIS development version is detected : {qgis_version[0]}.{qgis_version[1]}." - f"{Level.End}" + f"{Level.End}", ) qgis_version[1] = str(int(qgis_version[1]) + 1) print( f"{Level.Alert}" f"If needed, it will use {qgis_version[0]}.{qgis_version[1]} instead." - f"{Level.End}" + f"{Level.End}", ) return qgis_version @staticmethod - def server_cache_filename(cache_folder, server) -> Path: + def server_cache_filename(cache_folder: str, server: str) -> Path: """ Return the path for XML file. """ server, login, _ = Remote.credentials(server) filename = "" @@ -510,7 +510,7 @@ def credentials(cls, server: str) -> Tuple[str, str, str]: return urlunparse(u), '', '' - def all_credentials(self): + def all_credentials(self) -> Iterator[str, str, str]: """ Dirty hack to get all credentials for now… """ if self.list is None: self.remote_list() diff --git a/qgis_plugin_manager/utils.py b/qgis_plugin_manager/utils.py index d6d4282..dd63a4a 100644 --- a/qgis_plugin_manager/utils.py +++ b/qgis_plugin_manager/utils.py @@ -13,7 +13,7 @@ DEFAULT_QGIS_VERSION = "3.22" -def pretty_table(iterable, header) -> str: +def pretty_table(iterable: list, header: list) -> str: """ Copy/paste from http://stackoverflow.com/a/40426743/2395485 """ max_len = [len(x) for x in header] for row in iterable: @@ -23,16 +23,16 @@ def pretty_table(iterable, header) -> str: max_len[index] = len(str(col)) output = '-' * (sum(max_len) + 1) + '\n' output += '|' + ''.join( - [h + ' ' * (l - len(h)) + '|' for h, l in zip(header, max_len)]) + '\n' + [a_header + ' ' * (a_line - len(a_header)) + '|' for a_header, a_line in zip(header, max_len)]) + '\n' output += '-' * (sum(max_len) + 1) + '\n' for row in iterable: row = [row] if type(row) not in (list, tuple) else row output += '|' + ''.join( [ str(c) + ' ' * ( - l - len(str(c))) + '|' for c, l in zip( + a_line - len(str(c))) + '|' for c, a_line in zip( row, max_len) - ] + ], ) + '\n' output += '-' * (sum(max_len) + 1) + '\n' return output @@ -127,14 +127,14 @@ def qgis_server_version() -> str: print( f"{Level.Alert}" f"Cannot check version with PyQGIS, check your QGIS installation or your PYTHONPATH" - f"{Level.End}" + f"{Level.End}", ) print(f"Current user : {current_user()}") print(f'PYTHONPATH={os.getenv("PYTHONPATH")}') return '' -def sources_file(current_folder) -> Path: +def sources_file(current_folder: Path) -> Path: """ Return the default path to the "sources.list" file. The path by default or if it's defined with the environment variable. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..af3ee57 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +ruff diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e5eef0a..0000000 --- a/setup.cfg +++ /dev/null @@ -1,22 +0,0 @@ -[flake8] -max-line-length = 110 -max-complexity = 16 - -exclude = - .git, - venv, - .venv, - __pycache__, - -[isort] -multi_line_output = 3 -include_trailing_comma = True -use_parentheses = True -ensure_newline_before_comments = True -lines_between_types = 1 -skip = - .venv/, - venv/, - -[metadata] -license_files = LICENSE \ No newline at end of file diff --git a/setup.py b/setup.py index 0736da5..96e1720 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ sys.exit( "qgis-plugin-manager requires at least Python version " f"{python_min_version[0]}.{python_min_version[1]}.\n" - f"You are currently running this installation with\n\n{sys.version}" + f"You are currently running this installation with\n\n{sys.version}", ) # This string might be updated on CI on runtime with a proper semantic version name with X.Y.Z @@ -65,5 +65,5 @@ ], install_requires=[], python_requires=f">={python_min_version[0]}.{python_min_version[1]}", - include_package_data=True + include_package_data=True, ) diff --git a/test/test_full_install.py b/test/test_full_install.py index bf2ee2c..b4fbf6e 100644 --- a/test/test_full_install.py +++ b/test/test_full_install.py @@ -15,7 +15,7 @@ @unittest.skipIf(os.getenv('CI') != 'true', "Only run on CI") class FullInstallNetwork(unittest.TestCase): - def setUp(self) -> None: + def setUp(self): self.plugin_name = 'QuickOSM' self.repository = "https://plugins.qgis.org/plugins/plugins.xml?qgis=3.10" self.directory = Path('fixtures/plugins') @@ -28,7 +28,7 @@ def setUp(self) -> None: shutil.copy(Path(self.directory / 'sources.list'), Path(self.directory / 'sources.list.back')) - def tearDown(self) -> None: + def tearDown(self): if self.plugin_path.exists(): shutil.rmtree(self.plugin_path) @@ -57,7 +57,7 @@ def test_install_network(self): class FullInstallLocal(unittest.TestCase): - def tearDown(self) -> None: + def tearDown(self): destination = Path('fixtures/xml_files/file_protocol/minimal_plugin') if destination.exists(): shutil.rmtree(destination) @@ -77,7 +77,7 @@ def test_install_local(self): folder.joinpath('.cache_qgis_plugin_manager').mkdir(parents=True, exist_ok=True) shutil.copy( folder.joinpath('plugin.xml'), - folder.joinpath('.cache_qgis_plugin_manager/plugins.xml') + folder.joinpath('.cache_qgis_plugin_manager/plugins.xml'), ) remote = Remote(folder) diff --git a/test/test_local.py b/test/test_local.py index 8c414c2..9e37f88 100644 --- a/test/test_local.py +++ b/test/test_local.py @@ -11,7 +11,7 @@ class TestLocal(unittest.TestCase): - def setUp(self) -> None: + def setUp(self): self.local = LocalDirectory(Path('fixtures/plugins')) def test_list_existing_plugins(self): diff --git a/test/test_remote.py b/test/test_remote.py index 191a16e..034b3ed 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -34,8 +34,8 @@ def test_plugin_name_with_space_and_tags(self): plugin.search, [ 'data plotly', 'dataplotly', 'vector', 'python', 'd3', 'plots', 'graphs', 'datavis', - 'dataviz', 'data', 'plotly' - ] + 'dataviz', 'data', 'plotly', + ], ) # Test the search @@ -53,7 +53,7 @@ def test_search_with_space_in_name(self): 'Lizmap': '3.7.4', 'Lizmap server': '1.0.0', }, - plugins + plugins, ) plugin = self.remote.list_plugins.get('Lizmap server') @@ -62,7 +62,7 @@ def test_search_with_space_in_name(self): self.assertListEqual( plugin.search, - ['lizmap server', 'lizmapserver', 'web', 'cloud', 'lizmap', 'server'] + ['lizmap server', 'lizmapserver', 'web', 'cloud', 'lizmap', 'server'], ) # Test the search @@ -77,11 +77,11 @@ def test_parse_url(self): """ Test to parse a URL for login&password. """ self.assertTupleEqual( ('https://foo.bar/plugins.xml?qgis=3.10', 'login', 'pass'), - Remote.credentials("https://foo.bar/plugins.xml?qgis=3.10&username=login&password=pass") + Remote.credentials("https://foo.bar/plugins.xml?qgis=3.10&username=login&password=pass"), ) self.assertTupleEqual( ('https://foo.bar/plugins.xml?qgis=3.10', '', ''), - Remote.credentials("https://foo.bar/plugins.xml?qgis=3.10") + Remote.credentials("https://foo.bar/plugins.xml?qgis=3.10"), ) def test_clean_remote(self): @@ -89,17 +89,17 @@ def test_clean_remote(self): # "password", the keyword to look for self.assertEqual( "https://foo.bar/plugins.xml?qgis=3.10&username=login&password=%2A%2A%2A%2A%2A%2A", - Remote.public_remote_name("https://foo.bar/plugins.xml?qgis=3.10&username=login&password=pass") + Remote.public_remote_name("https://foo.bar/plugins.xml?qgis=3.10&username=login&password=pass"), ) # "pass", not the keyword to look for self.assertEqual( "https://foo.bar/plugins.xml?qgis=3.10&username=login&pass=pass", - Remote.public_remote_name("https://foo.bar/plugins.xml?qgis=3.10&username=login&pass=pass") + Remote.public_remote_name("https://foo.bar/plugins.xml?qgis=3.10&username=login&pass=pass"), ) # Nothing self.assertEqual( "https://foo.bar/plugins.xml?qgis=3.10", - Remote.public_remote_name("https://foo.bar/plugins.xml?qgis=3.10") + Remote.public_remote_name("https://foo.bar/plugins.xml?qgis=3.10"), ) @unittest.expectedFailure diff --git a/test/test_utils.py b/test/test_utils.py index 6f54154..5c80a0d 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -20,7 +20,7 @@ def test_similar_names(self): # typo wanted self.assertListEqual( ['Lizmap'], - similar_names('lizma', ['a', 'Lizmap', 'QuickOSM']) + similar_names('lizma', ['a', 'Lizmap', 'QuickOSM']), ) existing = ['data plotly', 'DATA PLOTLY', 'Data PLOTLY'] @@ -28,11 +28,11 @@ def test_similar_names(self): # lower case self.assertListEqual( existing, - similar_names('dataplotly', existing) + similar_names('dataplotly', existing), ) # upper case self.assertListEqual( existing, - similar_names('DATA PLOT LY', existing) + similar_names('DATA PLOT LY', existing), )