Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for pyproject.toml #228

Merged
merged 3 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ commands:

## Requirements

- The code is under a **git** repository (`git archive` is used to bundle the plugin)
- There is no uncommitted changes when doing a package/release (there is an option to allow this)
- A configuration at the top directory either in `.qgis-plugin-ci` or in `setup.cfg` with a `[qgis-plugin-ci]` section.
- The source files of the plugin are within a sub-directory. The name of this directory will be used for the zip file.
- The code is under a **git** repository (`git archive` is used to bundle the plugin).
- There is no uncommitted changes when doing a package/release (althought there is an option to bypass this requirement).
- A configuration at the top directory either in `.qgis-plugin-ci` or in `setup.cfg` or `pyproject.toml` with a `[qgis-plugin-ci]` section (see `docs/configuration/options.md` for details).
- The source files of the plugin are within a sub-directory with a `metadata.txt` file with the following fields:
- description
- qgisMinimumVersion
- repository
- tracker

See `parameters.py` for more parameters and details. Notice that the name of this directory will be used for the zip file.

## QRC and UI files

Expand Down
15 changes: 12 additions & 3 deletions docs/configuration/options.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Settings

The plugin must have a configuration, located at the top directory:
The plugin must have a configuration, located at the top directory; it can be either:

- either you use a `.qgis-plugin-ci` YAML file
- or you use a `[qgis-plugin-ci]` section in a `setup.cfg` file (which is used by many other tools).
- a YAML file named `.qgis-plugin-ci`
- an INI file named `setup.cfg` with a `[qgis-plugin-ci]` section
- a TOML file (= your actual `pyproject.toml` file) with a `[qgis-plugin-ci]` section.

In the configuration, you should at least provide the following configuration:

Expand Down Expand Up @@ -47,3 +48,11 @@ plugin_path = QuickOSM
github_organization_slug = 3liz
project_slug = QuickOSM
```
### Using TOML file `pyproject.toml`

```toml
[qgis-plugin-ci]
plugin_path = "qgis_plugin_ci_testing"
github_organization_slug = "opengisch"
project_slug = "qgis-plugin-ci"
```
2 changes: 1 addition & 1 deletion qgis_plugin_CI_testing/qgissettingmanager
30 changes: 2 additions & 28 deletions qgispluginci/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
#!/usr/bin/env python3

import argparse
import configparser
import logging
import os
from importlib.metadata import version

import yaml

from qgispluginci.changelog import ChangelogParser
from qgispluginci.exceptions import ConfigurationNotFound
from qgispluginci.parameters import Parameters
from qgispluginci.release import release
from qgispluginci.translation import Translation
Expand Down Expand Up @@ -169,28 +163,8 @@ def cli():

exit_val = 0

if os.path.isfile(".qgis-plugin-ci"):
# We read the .qgis-plugin-ci file
with open(".qgis-plugin-ci", encoding="utf8") as f:
arg_dict = yaml.safe_load(f)
else:
config = configparser.ConfigParser()
config.read("setup.cfg")
if "qgis-plugin-ci" in config.sections():
# We read the setup.cfg file
arg_dict = dict(config.items("qgis-plugin-ci"))
else:
# We don't have either a .qgis-plugin-ci or a setup.cfg
if args.command == "changelog":
# but for the "changelog" sub command, the config file is not required, we can continue
arg_dict = dict()
else:
raise ConfigurationNotFound(
".qgis-plugin-ci or setup.cfg with a 'qgis-plugin-ci' section have not been found."
)

parameters = Parameters(arg_dict)

# Initialize Parameters
parameters = Parameters.make_from(args=args)
# CHANGELOG
if args.command == "changelog":
try:
Expand Down
139 changes: 114 additions & 25 deletions qgispluginci/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@
# ##################################

# standard library
import configparser
import datetime
import logging
import os
import re
import sys
from typing import Any, Callable, Dict, Iterator, Optional, Tuple

import toml
import yaml

# 3rd party
from slugify import slugify

from qgispluginci.exceptions import ConfigurationNotFound

# ############################################################################
# ########## Globals #############
# ################################
Expand Down Expand Up @@ -97,9 +104,69 @@ class Parameters:

"""

def __init__(self, definition: dict):
@classmethod
def make_from(
cls, *, args: Optional[Any] = None, config_file: Optional[str] = None
) -> "Parameters":
"""
Instantiate from a config file or by exploring the filesystem
Accepts an argparse Namespace for backward compatibility.
"""
configuration_not_found = ConfigurationNotFound(
".qgis-plugin-ci or setup.cfg or pyproject.toml with a 'qgis-plugin-ci' section have not been found."
)

def explore_config() -> Dict[str, Any]:
if os.path.isfile(".qgis-plugin-ci"):
# We read the .qgis-plugin-ci file
with open(".qgis-plugin-ci", encoding="utf8") as f:
arg_dict = yaml.safe_load(f)
elif os.path.isfile("pyproject.toml"):
# We read the pyproject.toml file
with open("pyproject.toml", encoding="utf8") as f:
arg_dict = toml.load(f)
else:
config = configparser.ConfigParser()
config.read("setup.cfg")
if "qgis-plugin-ci" in config.sections():
# We read the setup.cfg file
arg_dict = dict(config.items("qgis-plugin-ci"))
else:
# We don't have either a .qgis-plugin-ci or a setup.cfg
if args and args.command == "changelog":
# but for the "changelog" sub command, the config file is not required, we can continue
arg_dict = dict()
else:
raise configuration_not_found
return arg_dict

def load_config(path_to_file: str) -> Dict[str, Any]:
if "setup.cfg" in path_to_file:
config = configparser.ConfigParser()
config.read(path_to_file)
return dict(config.items("qgis-plugin-ci"))

with open(path_to_file) as f:
if ".qgis-plugin-ci" in path_to_file:
return yaml.safe_load(f)
_, suffix = path_to_file.rsplit(".", 1)
if suffix == "toml":
contents = toml.load(f)
return contents["qgis-plugin-ci"]

raise configuration_not_found

if config_file:
config_dict = load_config(config_file)
else:
config_dict = explore_config()
return cls(config_dict)

def __init__(self, definition: Dict[str, Any]):
self.plugin_path = definition.get("plugin_path")
self.plugin_name = self.__get_from_metadata("name")

get_metadata = self.collect_metadata()
self.plugin_name = get_metadata("name")
self.plugin_slug = slugify(self.plugin_name)
self.project_slug = definition.get(
"project_slug",
Expand Down Expand Up @@ -146,22 +213,22 @@ def __init__(self, definition: dict):
# This tool can be used outside of a QGIS plugin to read a changelog file
return

self.author = self.__get_from_metadata("author", "")
self.description = self.__get_from_metadata("description")
self.qgis_minimum_version = self.__get_from_metadata("qgisMinimumVersion")
self.icon = self.__get_from_metadata("icon", "")
self.tags = self.__get_from_metadata("tags", "")
self.experimental = self.__get_from_metadata("experimental", False)
self.deprecated = self.__get_from_metadata("deprecated", False)
self.issue_tracker = self.__get_from_metadata("tracker")
self.homepage = self.__get_from_metadata("homepage", "")
self.author = get_metadata("author", "")
self.description = get_metadata("description")
self.qgis_minimum_version = get_metadata("qgisMinimumVersion")
self.icon = get_metadata("icon", "")
self.tags = get_metadata("tags", "")
self.experimental = get_metadata("experimental", False)
self.deprecated = get_metadata("deprecated", False)
self.issue_tracker = get_metadata("tracker")
self.homepage = get_metadata("homepage", "")
if self.homepage == "":
logger.warning(
"Homepage is not given in the metadata. "
"It is a mandatory information to publish "
"the plugin on the QGIS official repository."
)
self.repository_url = self.__get_from_metadata("repository")
self.repository_url = get_metadata("repository")

@staticmethod
def archive_name(
Expand All @@ -178,20 +245,42 @@ def archive_name(
experimental = "-experimental" if experimental else ""
return f"{plugin_name}{experimental}.{release_version}.zip"

def __get_from_metadata(self, key: str, default_value: any = None) -> str:
if not self.plugin_path:
return ""

def collect_metadata(self) -> Callable[[str, Optional[Any]], Any]:
"""
Returns a closure capturing a Dict of metadata, allowing to retrieve one
value after the other while also iterating over the file once.
"""
metadata_file = f"{self.plugin_path}/metadata.txt"
with open(metadata_file) as f:
for line in f:
m = re.match(rf"{key}\s*=\s*(.*)$", line)
if m:
return m.group(1)
if default_value is None:
logger.error(f"Mandatory key is missing in metadata: {key}")
sys.exit(1)
return default_value
metadata = {}
with open(metadata_file) as fh:
for line in fh:
split = line.strip().split("=", 1)
if len(split) == 2:
metadata[split[0]] = split[1]

def get_metadata(key: str, default_value: Optional[Any] = None) -> Any:
if not self.plugin_path:
return ""

value = metadata.get(key, None)
if value:
return value
elif default_value is not None:
return default_value
else:
logger.error(f"Mandatory key is missing in metadata: {key}")
sys.exit(1)

return get_metadata

def __iter__(self) -> Iterator[Tuple[str, Any]]:
"""Allows to represent attributes as dict, list, etc."""
for k in vars(self):
yield k, self.__getattribute__(k)

def __str__(self) -> str:
"""Allows to represent instances as a string."""
return str(dict(self))


# ############################################################################
Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pyqt5ac>=1.2,<1.3
python-slugify>=4.0,<8.1
pyyaml>=5.4,<6.1
transifex-python>=3.2,<4.0
toml>=0.10.2
14 changes: 14 additions & 0 deletions test/fixtures/.qgis-plugin-ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugin_path: qgis_plugin_CI_testing
github_organization_slug: opengisch
project_slug: qgis-plugin-ci
transifex_coordinator: geoninja
transifex_organization: pytransifex
translation_languages:
- fr
- it
- de
create_date: 1985-07-21

repository_url: https://github.com/opengisch/qgis-plugin-ci/

#lrelease_path: /usr/local/opt/qt5/bin/lrelease
4 changes: 4 additions & 0 deletions test/fixtures/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[qgis-plugin-ci]
plugin_path = "qgis_plugin_CI_testing"
github_organization_slug = "opengisch"
project_slug = "qgis_plugin_ci_testing"
4 changes: 4 additions & 0 deletions test/fixtures/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[qgis-plugin-ci]
plugin_path = qgis_plugin_CI_testing
github_organization_slug = opengisch
project_slug = qgis_plugin_ci_testing
42 changes: 31 additions & 11 deletions test/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@

class TestRelease(unittest.TestCase):
def setUp(self):
with open(".qgis-plugin-ci", encoding="utf8") as f:
arg_dict = yaml.safe_load(f)
self.parameters = Parameters(arg_dict)
self.setup_params = Parameters.make_from(
config_file=os.path.relpath("test/fixtures/setup.cfg")
)
self.qgis_plugin_config_params = Parameters.make_from(
config_file=os.path.relpath("test/fixtures/.qgis-plugin-ci")
)
self.pyproject_params = Parameters.make_from(
config_file=os.path.realpath("test/fixtures/pyproject.toml")
)
self.tx_api_token = os.getenv("tx_api_token")
self.github_token = os.getenv("github_token")
self.repo = None
Expand All @@ -59,15 +65,29 @@ def clean_assets(self):
print(f" delete {asset.name}")
asset.delete_asset()
if self.t:
self.t._t.delete_project(self.parameters.project_slug)
self.t._t.delete_project(self.qgis_plugin_config_params.project_slug)

def test_dict_from_config(self):
with self.subTest():
self.assertTrue(dict(self.qgis_plugin_config_params))
self.assertTrue(dict(self.pyproject_params))
self.assertTrue(dict(self.setup_params))

def test_release(self):
release(self.parameters, RELEASE_VERSION_TEST)
def test_release_from_dot_qgis_plugin_ci(self):
release(self.qgis_plugin_config_params, RELEASE_VERSION_TEST)

def test_release_from_pyproject(self):
print(self.pyproject_params)
release(self.pyproject_params, RELEASE_VERSION_TEST)

@unittest.skipIf(can_skip_test(), "Missing tx_api_token")
def test_release_with_transifex(self):
Translation(self.parameters, tx_api_token=self.tx_api_token)
release(self.parameters, RELEASE_VERSION_TEST, tx_api_token=self.tx_api_token)
Translation(self.qgis_plugin_config_params, tx_api_token=self.tx_api_token)
release(
self.qgis_plugin_config_params,
RELEASE_VERSION_TEST,
tx_api_token=self.tx_api_token,
)

def test_zipname(self):
"""Tests about the zipname for the QGIS plugin manager.
Expand Down Expand Up @@ -96,7 +116,7 @@ def test_zipname(self):
@unittest.skipIf(can_skip_test(), "Missing github_token")
def test_release_upload_github(self):
release(
self.parameters,
self.qgis_plugin_config_params,
RELEASE_VERSION_TEST,
github_token=self.github_token,
upload_plugin_repo_github=True,
Expand All @@ -123,8 +143,8 @@ def test_release_upload_github(self):

# compare archive file size
gh_release = self.repo.get_release(id=RELEASE_VERSION_TEST)
archive_name = self.parameters.archive_name(
self.parameters.plugin_path, RELEASE_VERSION_TEST
archive_name = self.qgis_plugin_config_params.archive_name(
self.qgis_plugin_config_params.plugin_path, RELEASE_VERSION_TEST
)
fs = os.path.getsize(archive_name)
print("size: ", fs)
Expand Down